您现在的位置是:首页 >技术杂谈 >Linux— 网络编程套接字网站首页技术杂谈

Linux— 网络编程套接字

晚风不及你的笑427 2024-07-04 00:01:02
简介Linux— 网络编程套接字

目录

预备知识

认识端口号

理解源端口号和目的端口号

认识TCP协议

认识UDP协议

 网络字节序

 socket编程接口

socket 常见API

sockaddr结构

 sockaddr 结构​编辑

 sockaddr_in 结构

 in_addr结构

地址转换函数

简单的UDP网络程序

实现一个简单的英译汉的功能

简易的远程命令行操作 

 简易的多人对话

简单的TCP网络程序

 TCP socket API 详解

  socket():

  bind():

  listen():

  accept():

  connect 

简单的TCP 网络通话

 多进程版

 多线程版

TCP协议通讯流程

TCP 和 UDP 对比


预备知识

认识端口号

我们知道IP地址可以标识主机唯一性,但是每一台主机上的客户或服务进程的唯一性用什么来标识呢?

为了更好的标识一台主机上服务进程的唯一性,采用端口号port,标识服务器进程,客户端进程的唯一性。

端口号(port)是传输层协议的内容.
端口号是一个2字节16位的整数;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
一个端口号只能被一个进程占用

 知道端口号之后有如下几个问题需要知道:

1. IP(标识全网主机唯一性) + 该主机上的端口号,标识服务器进程的唯一性。

  • 两个具有唯一性的进程进行网络通信时,本质上是在进行进程间通信,进程间通信就是让不同的进程看到同一份公共资源,那么这份公共资源就是网络。还有通信其实就是在做IO,所以我们的上网行为就分为两种:我要把数据发出去;我要接收别人的数据。

2. IP保证全网唯一,port保证在主机内部的唯一性。

3. 进程已经有了PID了,为什么还要有port呢?

  • 因为系统是系统,网络是网络,单独设置是为了让系统与网络之间解耦。
  • 需要客户端每次都能找到服务器进程,所以服务器进程的唯一性不能做任何改变,也就是说端口号是不能随意改变的。因为进程的PID重新启动时会发生变化,所以不能使用它(再说PID本身就不是为网络设定的)。
  • 不是所有进程都要提供网络服务或请求,但是所有进程都需要PID。

4. 通过进程和port,就可以找到对应的网络服务进程,那么底层操作系统是如何根据port找到指定的进程的?通过一种哈希的方式映射找到的。

5. 在网络通信过程中,我们除了要把数据发送给对方,也要把自己的ip和port发送给对方,因为对方给我们发数据时也需要ip和port。

理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁"。

认识TCP协议

这里我们先了解认识一下TCP(Transmission Control Protocol 传输控制协议)。。

  • 传输层协议
  • 有连接
  • 可靠传输          
  • 面向字节流

认识UDP协议

这里我们先了解认识一下UDP(User Datagram Protocol 用户数据报协议)。

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

 网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
     

 为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

  • 这些函数名很好记,h 表示host,n 表示network,l 表示32位长整数,s 表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

 socket编程接口

socket 常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)

  • int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)

  • int bind(int socket, const struct sockaddr *address,socklen_t address_len);

// 开始监听socket (TCP, 服务器)

  • int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)

  • int accept(int socket, struct sockaddr* address,socklen_t* address_len);

// 建立连接 (TCP, 客户端)

  • int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。

  • IPv4和IPv6的地址格式定义在 netinet/in.h 中,IPv4地址用 sockaddr_in 结构体表示,包括16位地址类型, 16位端口号和32位IP地址。
  • IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;


 sockaddr 结构

 sockaddr_in 结构

虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。

 in_addr结构

 in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。

地址转换函数

本篇文章只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址,但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:

in_addr转字符串的函数:

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void*addrptr。


代码示例:


简单的UDP网络程序

实现一个简单的英译汉的功能

服务器:

udpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <strings.h>
#include <errno.h>
#include <cstring>
#include <functional>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

namespace Server
{
    using namespace std;

    static const string defaultIP = "0.0.0.0";
    static const int gnum = 1024;
    enum { USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,OPEN_ERR};
    typedef function<void(int,string,uint16_t,string)> func_t;

    class udpServer
    {
    public:
        udpServer(const func_t& cb, const uint16_t& port,const string& ip = defaultIP)
            :_callback(cb),_port(port),_ip(ip),_sockfd(-1)
        {}
        void initServer()   //初始化服务器
        {
            // 1. 创建socket
            _sockfd = socket(AF_INET,SOCK_DGRAM,0);
            if(_sockfd == -1)
            {
                cerr << "socket error: " << errno  << " : " << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            cout << "socket success: " << " : " << _sockfd << endl;

            //2. 绑定 port,ip
            //未来服务器要明确的port,不能随意改变
            struct sockaddr_in local;   //在栈上定义了一个变量
            bzero(&local,sizeof(local));    //初始化
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);  //把port全部转为大端
            //inet_addr函数会帮我们作两件事:1.将string转为uint32_t, 2. htonl(),主机转网络
            local.sin_addr.s_addr = inet_addr(_ip.c_str()); 
            // local.sin_addr.s_addr = htonl(INADDR_ANY); //bind任意地址,服务器的真实写法

            int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
            if(n == -1)
            {
                cerr << "bind error: " << errno  << " : " << strerror(errno) << endl;
                exit(BIND_ERR);
            }
            //UDP Server 的预备工作完成
        }
        void start()    //运行服务器
        {
            //服务器的本质就是死循环
            char buffer[gnum];
            for(;;)
            {
                //读取数据
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                ssize_t s = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
                //1. 数据是什么2.谁发的
                if(s > 0)
                {
                    buffer[s] = 0;
                    string Clientip = inet_ntoa(peer.sin_addr);//inet_ntoa该函数做两件事:1.网络转主机,2.整数转string
                    uint16_t Clientport = ntohs(peer.sin_port);
                    string message = buffer;

                    cout << Clientip << "[" << Clientport << "]# "<< message << endl;
                    //我们只把数据打印出来就完了吗?要对数据做处理,回调函数
                    _callback(_sockfd,Clientip,Clientport,message);
                }
            }
        }
        ~udpServer()
        {}
    private:
        uint16_t _port;     //端口号
        string _ip;         //ip
        int _sockfd;        //套接字
        func_t _callback;   //回调
    };
}//namespace end Server

udpServer.cc

#include "udpServer.hpp"

#include <memory>
#include <signal.h>
#include <fstream>
using namespace Server;

const string dictTxt = "./dict.txt";
unordered_map<string,string> dict;
// 提示 运行格式
static void Usage(string proc)
{
    cout<<"Usage:
	" << proc <<" local_port

";
}
//切割dict.txt文本内容
static bool cutString(string& target,string* s1,string* s2,const string sep)
{
    auto pos = target.find(sep);
    if(pos == string::npos)
        return false;

    *s1 = target.substr(0,pos); //[)
    *s2 = target.substr(pos+sep.size()); //[)
    return true;
}
//输出字典内容
static void debugPrint()
{
    for(const auto& dt : dict)
    {
        cout << dt.first << " # " << dt.second << endl;
    }
}
//初始化字典
static void initDict()
{
    ifstream in(dictTxt,std::ios::binary);
    if(!in.is_open())
    {
        cerr << "open file " << dictTxt << " error" << endl;
        exit(OPEN_ERR);
    }
    string line;
    string key,value;
    while(getline(in,line))
    {
        // cout << line << endl;
        if(cutString(line,&key,&value,":"))
        {
            dict.insert(make_pair(key,value));
        }
    }
    in.close();
    cout << "load dict success " << endl;
}
//对捕捉的信号做自定义动作,该函数是 不退出就可以更新字典内容
void reload(int signo)
{
    (void)signo;
    initDict(); //初始化字典
}
//1. 一个翻译业务
void handlerMessage(int sockfd,string Clientip,uint16_t Clientport,string message)
{
    //就可以对message进行特定的业务处理,而不关心message怎么来的 -----  Server 通信和业务逻辑解耦
    //婴儿版的业务逻辑
    string response_message;
    auto iter = dict.find(message);
    if(iter == dict.end())
        response_message = "unkown";    //字典里找不到输入内容,就输出不知道
    else
        response_message = iter->second;
    
    //开始返回
    struct sockaddr_in client;
    bzero(&client,sizeof(client));

    client.sin_family = AF_INET;
    client.sin_port = htons(Clientport);
    client.sin_addr.s_addr = inet_addr(Clientip.c_str());

    sendto(sockfd,response_message.c_str(),response_message.size(),0,(struct sockaddr*)&client,sizeof(client));
    
    // cout << "服务器翻译结果# " << response_message << endl;
}



//  ./udpServer port
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    // string ip = argv[1];

    //1.
    signal(2,reload);//对2号信号做捕捉
    initDict(); //初始化字典
    // debugPrint();

    unique_ptr<udpServer> usvr(new udpServer(handlerMessage,port)); //1.
    // unique_ptr<udpServer> usvr(new udpServer(execCommand,port));     //2.
    // unique_ptr<udpServer> usvr(new udpServer(routemessage,port));    //3.

    usvr->initServer(); //初始化服务器
    usvr->start();      //运行服务器

    return 0;
}

客户端: 

 udpClient.hpp

#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <errno.h>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

namespace Client
{
    using namespace std;

    class udpClient
    {
    public:
        udpClient(const string& serverip,const uint16_t& serverport)
            :_serverip(serverip),_serverport(serverport),_sockfd(-1),_quit(false)
        {}
        void initClient()   //初始化客户端
        {
            // 1. 创建socket
            _sockfd = socket(AF_INET,SOCK_DGRAM,0);
            if(_sockfd == -1)
            {
                cerr << "socket error: " << errno  << " : " << strerror(errno) << endl;
                exit(2);
            }
            cout << "socket success: " << " : " << _sockfd << endl;

            //2. client 要不要bind?必须要。需不需要程序员自己显示的bind?不需要。
            //  由操作系统自动形成port进行bind,那么OS在什么时候bind,如何bind?
        }

        void run()      //运行客户端
        {
            struct sockaddr_in server;
            memset(&server,0,sizeof(server)); //bzero和memset这两个都可以
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            server.sin_port = htons(_serverport);

            string message;
            while(!_quit)
            {
                cout << "Please Enter# ";
                // cin >> message;
                getline(cin,message);
                // sendto :在套接字上发送消息
                sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));

                char buffer[1024];
                struct sockaddr_in temp;
                socklen_t temp_len = sizeof(temp);  //recvfrom: 从套接字接收消息
                ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&temp_len);
                if(n >= 0) buffer[n] = 0;
                cout << "服务器翻译结果# 
" << buffer << endl;
            }
        }
        ~udpClient()
        {}
    private:
        int _sockfd;    
        string _serverip;
        uint16_t _serverport;

        bool _quit;     //循环条件
    };
} //namespace end Client

 udpClient.cc

#include "udpClient.hpp"
#include <memory>

using namespace Client;

// 提示 运行格式
static void Usage(string proc)
{
    cout<<"Usage:
	" << proc <<" Server_ip Server_port

";
}
// ./udpClient Server_ip Server_port
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string Serverip = argv[1];
    uint16_t Serverport = atoi(argv[2]);

    unique_ptr<udpClient> uclt(new udpClient(Serverip,Serverport));

    uclt->initClient(); //初始化客户端
    uclt->run();        //运行客户端


    return 0;
}

简易的远程命令行操作 

 在原有的基础上增加一个接口即可

//2. 一个命令行业务
void execCommand(int sockfd,string Clientip,uint16_t Clientport,string cmd)
{   
    //禁止操作的命令
    if(cmd.find("rm") != string::npos || 
    cmd.find("mv") != string::npos || 
    cmd.find("rmdir") != string::npos)
    {
        cerr << Clientip << " : " << Clientport << " 正在做一个非法的操作:" << cmd << endl;
        return;
    }

    string response;
    FILE* fp = popen(cmd.c_str(),"r");  //popen = pipe + fork + exec
    if(fp == nullptr) response = cmd + "exec failed";
    char line[1024];
    while(fgets(line,sizeof(line),fp))
    {
        response += line;
    }
    pclose(fp);

    //开始返回
    struct sockaddr_in client;
    bzero(&client,sizeof(client));

    client.sin_family = AF_INET;
    client.sin_port = htons(Clientport);
    client.sin_addr.s_addr = inet_addr(Clientip.c_str());

    sendto(sockfd,response.c_str(),response.size(),0,(struct sockaddr*)&client,sizeof(client));
}

 简易的多人对话

udpServer.cc

//3. 多人对话服务
OnlineUser oluser;  //对象

void routemessage(int sockfd,string Clientip,uint16_t Clientport,string message)
{
    if(message == "online")
        oluser.addUser(Clientip,Clientport);
    if(message == "offline")
        oluser.delUser(Clientip,Clientport);
    if(oluser.isOnline(Clientip,Clientport))
    {
        //消息的路由
        oluser.broadcastMessage(sockfd,Clientip,Clientport,message);
    }
    else
    {
        struct sockaddr_in client;
        bzero(&client,sizeof(client));

        client.sin_family = AF_INET;
        client.sin_port = htons(Clientport);
        client.sin_addr.s_addr = inet_addr(Clientip.c_str());

        string response = "你还没有上线,请先上线!上线请运行:online 
";
        sendto(sockfd,response.c_str(),response.size(),0,(struct sockaddr*)&client,sizeof(client));
    }
}

udpClient.hpp 

#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <errno.h>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

namespace Client
{
    using namespace std;

    class udpClient
    {
    public:
        udpClient(const string& serverip,const uint16_t& serverport)
            :_serverip(serverip),_serverport(serverport),_sockfd(-1),_quit(false)
        {}
        void initClient()   //初始化客户端
        {
            // 1. 创建socket
            _sockfd = socket(AF_INET,SOCK_DGRAM,0);
            if(_sockfd == -1)
            {
                cerr << "socket error: " << errno  << " : " << strerror(errno) << endl;
                exit(2);
            }
            cout << "socket success: " << " : " << _sockfd << endl;

            //2. client 要不要bind?必须要。需不需要程序员自己显示的bind?不需要。
            //  由操作系统自动形成port进行bind,那么OS在什么时候bind,如何bind?
        }
        //线程代码
        static void* readMessage(void* args)
        {
            int sockfd = *(static_cast<int*>(args));
            pthread_detach(pthread_self());

            while(true)
            {
                char buffer[1024];
                struct sockaddr_in temp;
                socklen_t temp_len = sizeof(temp);  //recvfrom: 从套接字接收消息
                ssize_t n = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&temp_len);
                if(n >= 0) buffer[n] = 0;
                // cout << "服务器翻译结果# " << buffer << endl;
                cout << buffer << endl;
            }
            return nullptr;
        }
        void run()      //运行客户端
        {   //让线程帮我们发送消息
            pthread_create(&_reader,nullptr,readMessage,(void*)&_sockfd);

            struct sockaddr_in server;
            memset(&server,0,sizeof(server)); //bzero和memset这两个都可以
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            server.sin_port = htons(_serverport);

            string message;
            while(!_quit)
            {
                fprintf(stderr,"Please Enter# ");
                // cin >> message;
                getline(cin,message);
                // sendto :在套接字上发送消息
                sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));

            }
        }
        ~udpClient()
        {}
    private:
        int _sockfd;    
        string _serverip;
        uint16_t _serverport;

        bool _quit;     //循环条件

        pthread_t _reader;
    };
} //namespace end Client

onlineUser.hpp

#pragma once 

#include <iostream>
#include <string>
#include <cstring>
#include <unordered_map>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using namespace std;
class User
{
public:
    User(const string& ip,const uint16_t& port)
        :_ip(ip),_port(port)
    {}
    ~User()
    {}
    string ip(){ return _ip;}
    uint16_t port() { return _port;}
private:
    string _ip;
    uint16_t _port;
};

class OnlineUser
{
public:
    OnlineUser()
    {}
    void addUser(const string& ip,const uint16_t& port) //用户上线
    {
        string id = ip + "-" + to_string(port);
        users.insert(make_pair(id,User(ip,port)));
    }
    void delUser(const string& ip,const uint16_t& port)//用户下线
    {
        string id = ip + "-" + to_string(port);
        users.erase(id);
    }
    bool isOnline(const string& ip,const uint16_t& port)    //用户是否存在
    {
        string id = ip + "-" + to_string(port);
        return users.find(id) == users.end() ? false:true;
    }
    //给线长上所有用户转发消息
    void broadcastMessage(int sockfd,const string& ip,const uint16_t& port, const string& message)
    {
        for(auto& user:users)
        {
            struct sockaddr_in client;
            bzero(&client,sizeof(client));

            client.sin_family = AF_INET;
            client.sin_port = htons(user.second.port());
            client.sin_addr.s_addr = inet_addr(user.second.ip().c_str());
            string s = ip +"-" + to_string(port) + "# ";
            s += message;
            sendto(sockfd,s.c_str(),s.size(),0,(struct sockaddr*)&client,sizeof(client));

        }
    }
    ~OnlineUser()
    {}
private:
    unordered_map<string,User> users;
};

 UDP完整代码:lesson12/1_UDP · 晚风不及你的笑/MyCodeStorehouse - 码云 - 开源中国 (gitee.com)


简单的TCP网络程序

 TCP socket API 详解

  socket():

         socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;如果socket()调用出错则返回-1;应用程序可以像读写文件一样用read/write在网络上收发数据;

参数一:对于IPv4, family参数指定为AF_INET;
参数二:对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
参数三:protocol参数的介绍从略,指定为0即可

  bind():

        服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;服务器需要调用bind绑定一个固定的网络地址和端口号;

        bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听
        myaddr所描述的地址和端口号;bind()成功返回0,失败返回-1。

前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;

我们的程序中对myaddr参数是这样初始化的:

1. 将整个结构体清零;
2. 设置地址类型为AF_INET;
3. 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
4. 端口号为SERV_PORT, 我们定义为8080; 

  listen():

        listen()声明 sockfd 处于监听状态, 并且最多允许有 backlog 个客户端处于连接等待状态,如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5);
        listen()成功返回0,失败返回-1。

  accept():

        三次握手完成后,服务器调用accept()接受连接;如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;

        addr是一个传出参数,accept()返回时传出客户端的地址和端口号;如果给addr 参数传NULL,表示不关心客户端的地址;
        addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区); 

connect 

        客户端需要调用connect()连接服务器;
        connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
        connect()成功返回0,出错返回-1

简单的TCP 网络通话

tcpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h> 
#include <pthread.h>

#include "log.hpp"


namespace Server
{
    // using namespace std;

    static const uint16_t gport = 8080;
    static const int gbacklog = 5;

    class tcpServer;
    class threadData
    {
    public:
        threadData(tcpServer* self,const int& sock)
            :_self(self),_sock(sock)
        {}
    public:
        tcpServer* _self;
        int _sock;
    };
    class tcpServer
    {
    public:
        tcpServer(const uint16_t& port = gport)
            :_listensock(-1),_port(port)
        {}
        void initServer()
        {
            //1.创建socket文件套接字对象
            _listensock = socket(AF_INET,SOCK_STREAM,0);
            if(_listensock < 0)
            {
                logMessage(FATAL,"create socket error");
                exit(SOCKET_ERR);
            }
            logMessage(NORMAL,"create socket success");

            // 2.bind 绑定自己的网络信息
            struct sockaddr_in local;
            memset(&local,0,sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = INADDR_ANY;
            if(bind(_listensock,(struct sockaddr*)&local,sizeof(local)) < 0)
            {
                logMessage(FATAL,"bind socket error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL,"bind socket success");

            //3. 设置socket为监听状态
            if(listen(_listensock,gbacklog) < 0)
            {
                logMessage(FATAL,"listen socket error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL,"listen socket success");

        }
        void start()
        {
            for( ; ; )  //死循环
            {
                //4. Server获取新链接
                //sock 是用于和client进行通信的
                struct sockaddr_in peer;
                socklen_t peer_len = sizeof(peer);
                int sock = accept(_listensock,(struct sockaddr*)&peer,&peer_len);
                if(sock < 0)
                {
                    logMessage(ERROR,"accept error,next");
                    continue;
                }
                logMessage(NORMAL,"accept a new link success");
                cout << "sock: " << sock <<endl;
                //5. 未来通信就用这个sock,面向字节流的,后面全是文件操作
                // version 1
                 serviceIO(sock);
                 close(sock);    //对于一个已经使用完毕的sock,我们需要关闭它,不然会导致文件描述符泄漏
                
            }
        }
       

        void serviceIO(int sock)
        {
            char buffer[1024];
            while(true)
            {
                //暂时把读到的数据当字符串
                ssize_t n = read(sock,buffer,sizeof(buffer)-1);
                if(n > 0)
                {
                    buffer[n] = 0;
                    cout << "recv message: " << buffer << endl;
                    
                    string outbuffer = buffer;
                    outbuffer += " server[echo]";
                    write(sock,outbuffer.c_str(),outbuffer.size()); //多路转接
                }
                else if(n == 0)
                {
                    // 代表Client退出
                    logMessage(NORMAL,"client quit,me to!");
                    break;
                }
            }
        }
         
        ~tcpServer()
        {}
    private:
        int _listensock;    //不是用来数据通信的,它是监听链接是否到来的,用于获取新链接的
        uint16_t _port;
    } ;
    
}//namespace end Server 

tcpServer.cc

#include "tcpServer.hpp"
#include <memory>

using namespace Server;
// 提示 运行格式
static void Usage(string proc)
{
    cout<<"Usage:
	" << proc <<" local_port

";
}
// ./tcpserver local_port
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    unique_ptr<tcpServer> tsvr(new tcpServer(port));
    tsvr->initServer();
    tsvr->start();

    return 0;
}

tcpClient.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#include "log.hpp"  //日志信息

#define NUM 1024

namespace Client
{
    // using namespace std;
    class tcpClient
    {
    public:
        tcpClient(const string& serverip,const uint16_t serverport)
            :_sock(-1),_serverip(serverip),_serverport(serverport)
        {}
        void ininClinet()
        {
            //1.创建socket
            _sock = socket(AF_INET,SOCK_STREAM,0);
            if(_sock < 0)
            {
                logMessage(FATAL,"socket create error");//错误日志信息
                exit(2);
            }
            //2.tcp客户端也要bind,但是不用我们显示的bind,OS会帮我们bind的

        }
        void start()
        {

            struct sockaddr_in server;
            memset(&server,0,sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(_serverport);
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            //向服务器发起链接请求
            if(connect(_sock,(struct sockaddr*)&server,sizeof(server)) != 0)
            {
                logMessage(ERROR,"socket connect error");
            }
            else
            {
                string msg;
                while(true)
                {
                    cout << "Please# ";
                    getline(cin,msg);
                    write(_sock,msg.c_str(),msg.size());

                    char buffer[NUM];
                    int n = read(_sock,buffer,sizeof(buffer)-1);
                    if(n > 0)
                    {
                        //目前把读到的数据当成字符串
                        buffer[n] = 0;
                        cout << "Server回显# " << buffer << endl;
                    }
                    else
                    {
                        break;
                    }

                }
            }
        }
        ~tcpClient()
        {
            if(_sock >= 0) close(_sock);//可写,可不写
        }
    private:
        int _sock;
        string _serverip;
        uint16_t _serverport;
    };

}//namespace Client end

tcpClient.cc


#include "tcpClient.hpp"
#include <memory>

using namespace Client;

// 提示 运行格式
static void Usage(string proc)
{
    cout<<"Usage:
	" << proc <<" server_ip server_port

";
}
// ./tcpClient server_ip_ip server_port
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    unique_ptr<tcpClient> tpcl(new tcpClient(serverip, serverport));
    tpcl->ininClinet();
    tpcl->start();

    return 0;
}

再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信.原因是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接受新的请求.
当前的这个TCP, 只能处理一个连接, 这是不科学的.

 多进程版

只需要将tcpServer.hpp 中的第5 进行修改即可。

                //5. 未来通信就用这个sock,面向字节流的,后面全是文件操作
                // version 1
                // serviceIO(sock);
                // close(sock);    //对于一个已经使用完毕的sock,我们需要关闭它,不然会导致文件描述符泄漏
                //------------------------------------------------------------------------------------
                //version 2.1 多进程版
                pid_t id = fork();
                if(id == 0) //子进程
                {
                    close(_listensock);
                    if(fork() > 0) exit(0); //让孙子进程执行代码,子进程退出被父进程回收,孙子进程会变成孤儿进程

                    serviceIO(sock);
                    close(sock);
                    exit(0);
                }
                //父进程
                pid_t ret = waitpid(id,nullptr,0);
                close(sock);
                if(ret > 0)
                {
                    cout << "wait success: " << ret << endl;
                }
                //------------------------------------------------------------------------------------
                //version 2.2 多进程版 信号方式
                signal(SIGCHLD,SIG_IGN);    //信号忽略,忽略对子进程的管理

                pid_t id = fork();
                if(id == 0) //子进程
                {
                    close(_listensock);

                    serviceIO(sock);
                    close(sock);
                    exit(0);
                }
                close(sock);
                //------------------------------------------------------------------------------------

 改成多进程版后就不存在上面的问题了。

 多线程版

    class tcpServer;    //声明
    class threadData
    {
    public:
        threadData(tcpServer* self,const int& sock)
            :_self(self),_sock(sock)
        {}
    public:
        tcpServer* _self;
        int _sock;
    };

-------------------------------
下面是 class tcpServer 里的内容
        void start()
        {
            for( ; ; )  //死循环
            {
                //4. Server获取新链接
                //sock 是用于和client进行通信的
                struct sockaddr_in peer;
                socklen_t peer_len = sizeof(peer);
                int sock = accept(_listensock,(struct sockaddr*)&peer,&peer_len);
                if(sock < 0)
                {
                    logMessage(ERROR,"accept error,next");
                    continue;
                }
                logMessage(NORMAL,"accept a new link success");
                cout << "sock: " << sock <<endl;
                //5. 未来通信就用这个sock,面向字节流的,后面全是文件操作
         
                // //version 3 多线程版
                pthread_t tid;
                threadData* td = new threadData(this, sock);
                pthread_create(&tid,nullptr,thread_routinue,td);    //创建线程

                
            }
        }
        static void* thread_routinue(void* args)
        {
            pthread_detach(pthread_self());//线程分离
            threadData* td = static_cast<threadData*>(args);
            td->_self->serviceIO(td->_sock);
            delete td;
            close(td->_sock);

            return nullptr;
        }

 

单例线程池版+守护进程:

完整代码:lesson12/4_TCP(单例线程池) · 晚风不及你的笑/MyCodeStorehouse - 码云 - 开源中国 (gitee.com)

我们在登陆一个服务器时,它会为我们创建一个会话,一个会话中有且只能有一个前台任务,同时还有多个后台任务,任务是可以进行前后台转换的(fg,bg)。这些任务是会受到用户登录和注销影响的。也就是说我们写的服务器,我们注销以后其他人就访问不了了,这样是有问题的,创造者的登录登出应该对服务器不会产生影响,所以就需要守护进程。

守护进程也叫精灵进程,本质是孤儿进程的一种,它是某一个进程单独出来,自成一个会话,自成一个进程组,和终端设备无关。

 


TCP协议通讯流程

 下图是基于TCP协议的客户端/服务器程序的一般流程:

服务器初始化:

  1. 调用socket, 创建文件描述符;
  2. 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
  3. 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  4. 调用accecpt, 并阻塞, 等待客户端连接过来;

建立连接的过程:

  1. 调用socket, 创建文件描述符;
  2. 调用connect, 向服务器发起连接请求;
  3. connect会发出SYN段并阻塞等待服务器应答; (第一次);
  4. 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次);
  5. 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次);

用户发起链接请求后,链接的建立是由双方的操作系统自动完成的。

        这个建立连接的过程, 通常称为 三次握手。

数据传输的过程

  1. 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
  2. 服务器从accept()返回后立刻调用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
  3. 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
  4. 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
  5. 客户端收到后从read()返回, 发送下一条请求,如此循环下去;

断开连接的过程:

  1. 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
  2. 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
  3. read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
  4. 客户端收到FIN, 再返回一个ACK给服务器; (第四次)

同样,断开连接的过程也是双方操作系统自动完成的,我们只能决定什么时候开始建立链接和断开连接

        这个断开连接的过程, 通常称为 四次挥手。

所谓的链接说白了就是操作系统内部创建出来的链接结构体,它里面一定包含了链接在建立时链接相关的属性信息。那么服务端一定会有大量的链接到来,每到来一个链接,服务端就构建一个链接对象,然后将所有的链接对象在内核当中用特定的数据结构(比如链表)管理起来,至此就完成了对链接们建模的过程。

TCP 和 UDP 对比

  • 可靠传输 vs 不可靠传输
  • 有连接 vs 无连接
  • 字节流 vs 数据报

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。