• 【Network】网络编程套接字 —— socket编程


    我想游泳!我想游泳!我想游泳!不运动脑袋都不灵光了~

    有待继续补充,但还是先传上来~

    我们这篇文章究竟在干什么呢?就是从0开始编写应用层,并不是使用应用层! 我们将要使用的的socket(), bind(), listen(), accept(), connect(), read/write都是一系列系统调用接口。

    正文开始@小边小边不秃头

    1. 预备知识

    1.1 IP

    IP地址(公网IP)可以唯一标识互联网中的一台主机。

    源IP, 目的IP:对一个报文来讲,从哪里来,到哪里去。它最大的意义在于,指导一个报文如何进行路径选择;到哪里去,本质就是让请我们根据目标进行路径选择的依据。下一跳设备(mac地址的变化)。

    1.2 端口号

    事实上,数据从主机A到达主机B,不是目的;数据到达目标主机B的一个进程,并提供数据处理服务,才是最终目的。

    数据刚开始时,从哪里来呢?并不是在计算机上凭空产生,而是由人通过客户端产生数据。

    qq的app客户端(进程) ←→ qq的服务器(进程)

    所有的网络通信,站在普通人的角度,都是人和人之间的通信;站在技术人员的视角,我们学的网络通信,本质是进程间通信

    IP仅仅是解决了两台物理机器间相互通信,但是怎样使双方用户能看到发送和接收的数据呢?—— 端口号,用来唯一标识一台机器上唯一的一个进程。

    综上,IP + PORT = 能够标识互联网中的唯一一个进程,那么互联网中各自唯一的一对进程就可以进行进程间通信。如果把整个网络看做一个大的OS,所有的网络上的上网行为,基本都是在这样一个大的OS内进行进程间通信
    I P 地址 + p o r t 端口号 = s o c k e t IP 地址 + port端口号 = socket IP地址+port端口号=socket

    • PID vs port:啊喂喂!那进程的PID不也是标识唯一进程的吗?哦哦,这就相当于身份证号 vs 学号 —— 解耦。

    • IP vs port:要进行通信,本质:①先找到目标主机 ②再找到该主机上的服务(进程)

    进程具有独立性,进程间通信的前提是:让不同的进程,看到同一份资源 —— 网络。

    另外,一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定。

    1.3 TCP协议

    TCP (Transmission Control Protocol 传输控制协议)

    • 传输层协议

    • 有连接

    • 可靠传输

    • 面向字节流

    TCP的一些细节问题后面详谈。

    1.4 UDP协议

    UDP (User Datagram Protocol 用户数据报协议)

    • 传输层协议

    • 无连接

    • 不可靠传输

    • 面向数据报

    这里所谓“可不可靠”是中性词,因为可靠意味着要花费更多资源,具体应结合应用场景。选择时如果不需UDP特定的优点,那么统一用TCP。

    1.5 网络字节序

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

    发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,但是并不知道是大/小端机器,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定,先发出的数据是低地址,后发出的数据是高地址。

    TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节.

    • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;

    • 如果当前发送主机是小端, 就需要先将数据转成大端; 如果是大端,直接发送即可

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

    #include 
    
    uint32_t htonl(uint32_t hostlong);
    uint16_t hton3(uint16_t hostshort);
    uint32_t ntohl(uint32_t netlong);
    uint16_t ntoh3(uint16_t netshort);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • h代表的是host,n代表的是network;s代表的是16位的短整型,l代表的是32位长整形
    • 如果主机是小端字节序,函数会对参数进行处理,进行大小端转换
    • 如果主机是大端字节序,函数不会对这些参数处理,直接返回

    1.6 socketaddr

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    网络的通信方式有很多种,基于IP的网络通信AF_INET,原始套接字,域间套接字,为了系统结构统一化,我们引入sockaddrs结构,这是一个通用接口 ——

    在这里插入图片描述

    2. udp套接字

    我们希望 —— client发送信息,server收到信息,“处理信息”,并将结果发回给client,并回显

    代码贴在2.?.4了,宝子们,请搭配2.?.1~2.?.3 讲解食用~

    2.1 udp echo server

    2.1.1 创建套接字

    #include           /* See NOTES */
    #include 
    
    int socket(int domain, int type, int protocol);
    
    • 1
    • 2
    • 3
    • 4

    返回值:On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.

    参数:

    • domain:通信方式,协议家族

    • type:套接字类型,用户数据报式

    • protocol:套接字采用的协议类型。在UDP和TCP这里全部设置为0

    2.1.2 bind绑定IP和端口号

    作为一个服务器,要让客户知道服务器的地址(IP+port),一般服务器的port,必须是众所周知(不仅是人,还有各种软件,app,浏览器等)且不能被轻易改变的。

    // man 2 bind
    #include           /* See NOTES */
    #include 
    
    int bind(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    返回值:On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

    参数:

    • sockfd:刚刚创建好的套接字 file descriptor

    • addr:通用接口struct sockaddr,我们要使用的是struct sockaddr_in,需强转传入,类似C++多态。需要用户指定服务器相关的socket信息,在该结构体内设置套接字协议家族、端口、IP,传给操作系统,它的字段如下 ,需要包含#include#include这两个头文件 ——

      • 在设置端口号时,此处的端口号,是计算机上的变量,是主机序列,需要转成网络序列

        //man htons
        #include 
        
        uint32_t htonl(uint32_t hostlong);
        uint16_t htons(uint16_t hostshort);
        uint32_t ntohl(uint32_t netlong);
        uint16_t ntohs(uint16_t netshort);
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7

        需要将人识别的点分十进制字符串风格的IP地址,转化为4字节的整数IP,并考虑大小端,我们直接调用系统函数即可

        //man inet_addr
        #include 
        #include 
        #include 
        
        in_addr_t inet_addr(const char *cp); //这一个函数就能完成如上ab两个任务
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6

        但是云服务器,不允许用户直接bind公网IP,另外,实际正常编写时,也不会指明IP,而是直接设置为0 ——

        //#define	INADDR_ANY		((in_addr_t) 0x00000000)
        local.sin_addr.s_addr = INADDR_ANY; //0
        
        • 1
        • 2

        INADDR_ANY望文生义就是任意的地址。如果你bind的是确定的IP(主机),意味着只有发到该IP主机上的数据才会交给你的网络进程,但是一般服务器,可能有多张网卡配置多个IP。我们需要的不是某个IP上的数据,我们需要的是所有发送到该主机该端口的数据。

    • addrlen:传入结构体的大小

    2.1.3 提供服务

    读数据,我们不再以文件方式读,还是要通过接口 ——

    💛 receive a message from a socket

    #include 
    #include 
    
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    返回值:These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error. The return value will be 0 when the peer has performed an orderly shutdown.

    参数:

    • buffer自己开一段空间,接收信息

    • flags:读的方式,我们默认为0即可。重要的是最后两个参数 ——

    • src_addr输入输出型参数。 给一段空间,填入和你服务器通信的客户端的信息,即表明是谁给你发送的数据。

    • addrlen

    把拿到的数据处理完,返回给client

    💛 send a message on a socket

    #include 
    #include 
    
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                  const struct sockaddr *dest_addr, socklen_t addrlen);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    返回值:These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error. The return value will be 0 when the peer has performed an orderly shutdown.

    参数:

    • dest_addr:对端的套接字信息,把我们获取到的传回去即可。
    • addrlen:套接字长度

    2.1.4 代码

    至此我们服务端代码写完了嗷!请搭配讲解食用,我再把逻辑给你们捋捋,我的宝子们~

    • 创建套接字
    • 绑定ip和端口号(ip先写127.0.0.1,端口号从命令行参数中获取)
    • 提供服务
      • recvfrom 接收客户端信息:开一段空间接收信息 and 获得客户端sockaddr信息以便发回处理信息的结果
      • sendto 发回信息:拿着上面拿到的客户端信息,发回处理信息
    #include
    #include
    #include
    #include
    #include
    #include //struct sockaddr_in
    #include  //struct sockaddr_in
    
    //const uint16_t port = 8080;
    
    //./udp_server port
    void Usage(std::string proc)
    {
        std::cout << "Usage: \n\t" << proc << "port" << std::endl; 
    }
    
    int main(int argc, char* argv[])
    {
        if(argc != 2)
        {
            Usage(argv[0]);
            return 0;
        }
    
        int16_t port = atoi(argv[1]);
    
        // udp_server
        //1. 创建套接字 - 打开网络文件
        int sock = socket(AF_INET, SOCK_DGRAM, 0);
        if(sock < 0)
        {
            std::cerr << "socket create error:" << errno << std::endl;
            return 1;
        }
        //std::cout << "socket: " << sock << std::endl; //如愿是3
        
        //2.给该服务器绑定端口和ip(特殊处理)
        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_port = htons(port); //此处的端口号,是计算机上的变量,是主机序列,需要转成网络序列
        //a. 需要将人识别的点分十进制字符串风格的IP地址,转化为4字节的整数IP
        //b. 考虑大小端
        //local.sin_addr.s_addr = inet_addr("***.**.***.***"); //点分十进制,字符串风格[0-255].[0-255].[0-255].[0-255]
        //但是 —— 
        local.sin_addr.s_addr = INADDR_ANY; //0
    
        if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind error: " << errno << std::endl;
            return 2;
        }
    
        //3.提供服务
        bool quit = false;
        #define NUM 1024
        char buffer[NUM];
        while(!quit) //网络通信都是死循环
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            //注意:我们默认认为通信的数据石双芳在互发字符串
            ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            if(cnt > 0)
            {
                buffer[cnt] = 0;
                std:: cout << "client# " << buffer << std::endl;
                //根据用户输入,构建一个新的返回字符串儿发回
                std::string echo_hello = buffer;
                echo_hello += "...";
                sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr*)&peer, len);
            }
            else
            {
                //TODO
            }
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 在网络通信中,只有报文大小,或者是字节流中字节的个数,没有C/C++字符串这样的概念(虽然我们后序可能经常遇到这样的状况)

    2.2 udp echo client

    2.2.1 创建套接字

    一模一样儿~

    2.2.2 客户端为什么不需要显式bind

    首先,客户端必须有IP和port,但是客户端不需要显式bind。

    客户端不是不能bind,它也可以bind,但是我们并不建议。

    那服务器为什么要 bind 呢?因为服务器总是被动方,需要在一个众所周知的端口上等待连接请求,而且作为服务器它的端口号应该是固定且明确的,服务器bind一个端口就表示会在这个端口提供一些特殊的服务。而客户端它是主动发起方,我们并不关心是客户端的哪个端口和服务器建立了连接,内核会自动为我们分配一个随机的不冲突的端口号;如果我们对客户端bind的话,反而有可能已经被占用导致端口冲突。

    2.2.3 使用服务

    客户端访问服务器必须要知道ip和端口号 ——

    ./udp_client server_ip server_port //ip和port我们都已知**.***.**.***和写死的8080
    
    • 1

    客户端并不知道把数据发给谁,所以我们获取一下命令行参数,并填入sockaddr_in结构体中。

    2.2.4 代码

    client.cc

    • 创建套接字
    • 使用服务
      • 发送服务请求:用户从键盘输入请求
      • 获取请求结果:
    #include
    #include
    #include
    #include
    #include 
    #include  
    
    // ./udp_client server_ip server_port
    void Usage(std::string proc)
    {
        std::cout << "Usage: \n\t" << proc << "server_ip server_port" << std::endl;
    }
    
    int main(int argc, char* argv[])
    {
        if(argc != 3)
        {
            Usage(argv[0]);
            return 0;
        }
        //b.你要发送给谁?填充服务器的ip和端口号
        struct sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(atoi(argv[2])); //字符串转整数,主机序列转网络序列
        server.sin_addr.s_addr = inet_addr(argv[1]); //字符串格式转四字节整数
    
        //1. 创建套接字 - 打开网络文件
        int sock = socket(AF_INET, SOCK_DGRAM, 0);
        if(sock < 0)
        {
            std::cerr << "socket create error:" << errno << std::endl;
            return 1;
        }
    
        //2. 客户端需要显式的 bind吗?nope
        //3.使用服务
        while(1)
        {
            //a.发送的客户端的数据 - 键盘输入,相当于请求服务
            std::string message;
            std::cout<< "输入# ";
            std::cin >> message; 
    
            sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
    
            //此处tmp就是一个摆设~
            struct sockaddr_in tmp;
            socklen_t len = sizeof(tmp);
            char buffer[1024];
            ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&tmp, &len);
            if(cnt > 0)
            {
                buffer[cnt] = 0;
                std::cout << "server echo# " << buffer << std::endl;
            }
            else
            {
                //TODO
            }
    
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    2.3 效果展示 & 补充

    注:如果你编写的udp无法通信,云服务器开放服务首先需要开放端口,默认的云平台是没有开放特定端口!需要所有者在网页后端->安全组->开放端口。

    如果我们把client发来的字符串当做命令,server执行命令,并把执行结果返回给client,我们就来实现一个究极草率版本的shell命令行解释器 ——

    #include 
    
    FILE *popen(const char *command, const char *type);
    
    int pclose(FILE *stream);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    fork创建子进程执行command,执行结果以文件返回;通过管道pipe实现父子进程通信。type表示打开文件的方式。

    在这里插入图片描述

    宝子们更改后的代码附在这里咯,这次我们不把端口号写死,而是通过命令行参数传入 ——

    sever.cc

    #include
    #include
    #include
    #include
    #include
    #include
    #include //struct sockaddr_in
    #include  //struct sockaddr_in
    
    //const uint16_t port = 8080;
    
    //./udp_server port
    void Usage(std::string proc)
    {
        std::cout << "Usage: \n\t" << proc << "port" << std::endl; 
    }
    
    int main(int argc, char* argv[])
    {
        if(argc != 2)
        {
            Usage(argv[0]);
            return 0;
        }
    
        int16_t port = atoi(argv[1]);
    
        // udp_server
        //1. 创建套接字 - 打开网络文件
        int sock = socket(AF_INET, SOCK_DGRAM, 0);
        if(sock < 0)
        {
            std::cerr << "socket create error:" << errno << std::endl;
            return 1;
        }
        
        //2.给该服务器绑定端口和ip(特殊处理)
        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_port = htons(port); //此处的端口号,是计算机上的变量,是主机序列,需要转成网络序列
        local.sin_addr.s_addr = INADDR_ANY; //0
        if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind error: " << errno << std::endl;
            return 2;
        }
    
        //3.提供服务
        bool quit = false;
        #define NUM 1024
        char buffer[NUM];
        while(!quit) //网络通信都是死循环
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            //注意:我们默认认为通信的数据石双芳在互发字符串
            ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            if(cnt > 0)
            {
                buffer[cnt] = 0;
                std::cout << "client# " << buffer << std::endl;
                
                FILE* fp = popen(buffer, "r");
                //读文件
                std::string echo_hello;
                char line[1024] = {0};
                while(fgets(line, sizeof(line), fp) != NULL)
                {
                    echo_hello += line;
                }
    
                pclose(fp);
                sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr*)&peer, len);
            }
            else
            {
                //TODO
            }
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    client.cc

    #include
    #include
    #include
    #include
    #include
    #include 
    #include  
    
    //./udp_client server_ip server_port
    void Usage(std::string proc)
    {
        std::cout << "Usage: \n\t" << proc << " server_ip server_port" << std::endl;
    }
    
    int main(int argc, char* argv[])
    {
        if(argc != 3)
        {
            Usage(argv[0]);
            return 0;
        }
        //你要发送给谁?根据命令行参数填充服务器的ip和端口号
        struct sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(atoi(argv[2])); //字符串转整数,主机序列转网络序列
        server.sin_addr.s_addr = inet_addr(argv[1]); //字符串格式转四字节整数
    
        //1. 创建套接字 - 打开网络文件
        int sock = socket(AF_INET, SOCK_DGRAM, 0);
        if(sock < 0)
        {
            std::cerr << "socket create error:" << errno << std::endl;
            return 1;
        }
    
        //2. 客户端需要显式的 bind吗?nope
        //3.使用服务
        while(1)
        {
            // 按行 读文件中的字符串儿
            std::cout << "Myshell$ ";
            char line[1024];
            fgets(line, sizeof(line), stdin);
    
            sendto(sock, line, strlen(line), 0, (struct sockaddr*)&server, sizeof(server));
    
            //此处tmp就是一个摆设~
            struct sockaddr_in tmp;
            socklen_t len = sizeof(tmp);
            char buffer[1024];
            ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&tmp, &len);
            if(cnt > 0)
            {
                buffer[cnt] = 0;
                std::cout << buffer << std::endl;
            }
            else
            {
                //TODO
            }
    
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    3. tcp套接字

    3.1 tcp echo server

    3.1.1 创建套接字 & bind

    #include           /* See NOTES */
    #include 
    
    int socket(int domain, int type, int protocol);
    
    • 1
    • 2
    • 3
    • 4

    接口同udp,只不过tcp的套接字类型是SOCK_STREAM,流式套接,可以通过文件的读写。

    事实上,这段代码和udp的几乎没有差别,这就是统一接口的好处。

    3.1.2 监听

    因为tcp是面向连接的,即通信时必须先建立连接。啥意思呢?比如,我给我闺蜜发快递,我只要知道她的电话和地址,直接给她发就行了 - tcp(我这货还把她电话号记错了艹 ;而QQ电话,我们要互相打招呼再打~ - udp

    • 通信前,需要先建立连接。这就意味着一定有一个人主动建立连接(客户端),有一个人被动接受连接(服务端)
    • 然后才能通信

    作为server,需要周而复始不间断的等待客户的到来,(在udp中我们写了一个死循环).

    在这里,我们就需要不断的给用户提供一个建立连接的功能 —— 设置套接字设置为listen状态,本质是允许用户连接

    listen - listen for connections on a socket

    // man 2 listen
    #include           /* See NOTES */
    #include 
    
    int listen(int sockfd, int backlog);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    返回值:On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

    参数:backlog后面详谈

    3.1.3 accept获取连接

    accept - accept a connection on a socket

    // man 2 accept
    #include           /* See NOTES */
    #include 
    
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    返回值:On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket. On error, -1 is returned, and errno is set appropriately. 也是文件描述符?!!

    • socket监听套接字,拉客少年张三
    • 返回值:提供IO服务的套接字,店内服务员

    • addr:它们的含义类似于recvfrom的作用,是输入输出型参数,我非常想知道是谁连接的我,毕竟我还需要把处理信息返回
      • 输入:缓冲区
      • 输出:对端(客户端)的socket信息(主要是IP和端口)

    3.1.4 提供服务

    因为tcp是面向字节流的,就如同文件一般,可以进行正常的读写,read/write、recv/send…

    #include 
    
    ssize_t read(int fd, void *buf, size_t count);
    ssize_t write(int fd, const void *buf, size_t count);
    
    • 1
    • 2
    • 3
    • 4

    3.1.5 代码

    #include
    #include
    #include
    #include
    #include           
    #include 
    #include 
    #include  
    #include 
    
    // ./udp_server port
    void Usage(std::string proc)
    {
        std::cout << "Usage: " << proc << "port" << std::endl;
    }
    
    int main(int argc, char* argv[])
    {
        if(argc != 2)
        {
            Usage(argv[0]);
            return 1;
        }
    
        //tcp_server
        //1.创建套接字
        int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
        if(listen_sock < 0)
        {
            std::cerr << "socket error: " << errno << std::endl;
            return 2;
        }
    
        //2.bind 绑定
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local)); //对结构体变量进行清空
        local.sin_family = AF_INET;
        local.sin_port = htons(atoi(argv[1]));
        local.sin_addr.s_addr = INADDR_ANY;
        // 用户栈上的信息填入内核 - bind
        if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind error" << errno << std::endl;
            return 3;
        }
    
        //3.监听
        const int back_log = 5;
        if(listen(listen_sock, back_log) < 0)
        {
            std::cerr << "listen error" << std::endl;
            return 4;
        }
    
        for(;;)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
            if(new_sock < 0)
            {
                //获取新链接失败...move on!
                continue;
            }
            std::cout << "get a new link..." << std::endl;
    
            //提供服务
            //version1: 单进程版本,没人使用
            while(true)
            {
                char buffer[1024];
                memset(buffer, 0, sizeof(buffer)); //缓冲区清0
                ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
                if(s > 0)
                {
                    buffer[s] = 0; //将获取的内容当做字符串
                    std::cout << "client# " << buffer << std::endl;
                    
                    //返回处理信息的结果
                    std::string echo_string = ">>>server<<<, ";
                    echo_string += buffer;
    
                    write(new_sock, echo_string.c_str(), echo_string.size());
                }
                else if(s == 0)
                {
                    //对端关闭
                    std::cout << "client quit..." << std::endl;
                    break;
                }
                else
                {
                    //读失败
                    std::cerr << "read error" << std::endl;
                    break;
                }
            }
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100

    3.2 tcp echo client

    3.2.1 创建套接字

    一样一样!!

    client无需显式bind,client也无需listen和accept,但是客户端需要connect!

    3.2.2 connect发起连接

    connect - initiate a connection on a socket

    #include           /* See NOTES */
    #include 
    
    int connect(int sockfd, const struct sockaddr *addr,
               socklen_t addrlen);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    返回值:If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately.

    • addr:server的信息

      注:inet_addr这个函数做两件事儿:将点分十进制点分十进制字符串风格IP,转化为4字节的IP;将4字节由主机序列转化为网络序列。

    3.2.3 使用服务

    • 从键盘输入
    • 把server传来的处理结果回显

    3.2.4 代码

    
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    
    // ./tcp_client server_ip server_port /*要访问服务器的信息*/
    void Usage(std::string proc)
    {
        std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
    }
    int main(int argc, char* argv[])
    {
        if(argc != 3)
        {
            Usage(argv[0]);
            return 1;
        }
        std::string svr_ip = argv[1];
        uint16_t svr_port = atoi(argv[2]);
        
        //1.创建套接字
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if(sock < 0)
        {
            std::cerr << "socket error: " << std::endl;
            return 2;
        }
        
        //2.connect发起连接
        struct sockaddr_in server;
        memset(&server, 0 ,sizeof(server));
        // bzero(&server, sizeof(server)); //另一种结构体清0的方式,不过不推荐
        server.sin_family = AF_INET;
        server.sin_port= htons(svr_port); //点分十进制的字符串儿格式 转化为 四字节网络大端地址,见2.1.2
        server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
    
        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
        {
            std::cout << "connect server failed" << std::endl;
            return 3;
        }
        std::cout << "connect success!" << std::endl;
    
        //3.请求服务
        while(true)
        {
            std::cout << "client, Please Enter# ";
            char buffer[1024];
            fgets(buffer, sizeof(buffer)-1, stdin);
    
            write(sock, buffer, strlen(buffer));
    
            ssize_t s = read(sock, buffer, sizeof(buffer)-1);
            if(s > 0)
            {
                buffer[s] = 0;
                std::cout << "server echo# " << buffer << std::endl;
            }
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    3.2.5 效果展示

    但是现在只能给一个人儿提供服务,这个单进程版本,没人儿使用。

    接下来要介绍一系列调优版本。

    3.4 多进程版本server

    多线程及线程池版本代码等待补充,问就是我信号、多线程没学完,还想快点先把进度推下去,主要目的在于在这半个月之内希望对计算机网络有大致的认识~

    3.4 多线程版本server

    3.5 线程池版本server

    3.6 tcp协议通信流程

    1. 创建socket的过程,socket()本质是打开文件 —— 仅仅有系统相关的内容
    2. bind():struct sockaddr_in → ip、port,本质是IP+port和socket文件信息进行关联
    3. listen():本质是设置该socket稳健的状态,允许别人来连接我
    4. accept():获取新链接到应用层,是以fd为代表的。所谓新链接就是,当有很多链接连上我们的服务器时,OS中会存在大量的链接,definitely OS要管理这些已经建立好的链接:先描述,再组织。所谓的“连接“,在OS层面,本质就是一个个描述连接的结构体。
    5. read/write:本质就是进行网络通信,但是对于用户讲,相当于进行普通的文件读写。
    6. close(fd):关闭文件。①系统层面,释放曾经申请的文件资源、连接资源等 ②网络层面,通知对方,我的连接已经关闭了
    7. connect():本质是发起连接。①系统层面,就是构建一个请求报文发送过去 ②网络层面,发起tcp链接的三次握手
    8. close(),client && server本质在网络层面,就是进行四次挥手

  • 相关阅读:
    短链接网站系统设计与实践
    【面试系列】后端开发工程师 高频面试题及详细解答
    已解决python -m pip install --upgrade pip命令升级报错
    JavaScript开发重型跨平台应用以及架构
    安卓常见设计模式9------外观模式(Kotlin版)
    “第五十天” 机组--数据的表示
    Linux 虚拟化
    Java实现Excel导入导出
    ES6~ES13新特性(一)
    ant design pro git提交error; Angular 团队git提交规范
  • 原文地址:https://blog.csdn.net/qq_54851255/article/details/126533164