Linux网络编程套接字

一、预备知识

1. IP 地址

  • IP 协议有两个版本IPv4 和 IPv6 。在文章中凡是提到 IP 协议没有特殊说明的默认都是指 IPv4 。

  • 对于 IPv4 来说IP 地址是一个 4 字节的32位整数。

  • 我们通常也使用 “点分十进制” 的字符串表示IP地址例如 180.101.50.172用点分割的每一个数字表示一个字节范围是 [0, 255] 。

  • 公网 IP:通常用来唯一地标识互联网中的一台主机。

  • 源 IP 和目的 IP:对一个报文来讲回答了从哪里来到哪里去的问题最大的意义是指导一个报文该如何进行路径选择。

2.端口号

  • 端口号(port是传输层协议的内容。它是一个 2 字节 16 位的整数用来唯一地标识一台主机上的一个进程。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

进程具有独立性进程间通信的前提工作:先得让不同的进程看到同一份资源这份资源在这里就是网络

源端口号和目的端口号:描述数据是哪个进程发的要发给哪个进程。

一个进程可以关联多个端口号但是一个端口号不可以关联多个进程这个可以由端口号的概念得出。

3. TCP 协议和 UDP 协议

我们需要先对 TCP 协议和 UDP 协议有一个直观的认识后面再详细讨论。

  • TCP(Transmission Control Protocol传输控制协议
     ① 传输层协议。
     ② 有连接。
     ③ 可靠传输。
     ④ 面向字节流。

  • UDP(User Datagram Protocol用户数据报协议
     ① 传输层协议。
     ② 无连接。
     ③ 不可靠传输。
     ④ 面向数据报。

TCP 的可靠和 UDP 的不可靠都是中性词客观的没有谁好谁不好只有谁更合适。

4.网络字节序

我们已经知道内存中的多字节数据相对于内存地址有大端和小端之分磁盘文件中的多字节数据相对于文件中的偏移地址也有大端和小端之分。网络数据流同样有大端和小端之分那么为了避免网络通信中不同主机大小端不一致的问题应如何定义网络数据流的地址呢?

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

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

在这里插入图片描述
以上函数的作用:
 ① 如果主机是小端字节序这些函数将参数转换为大端字节序然后返回。
 ② 如果主机是大端字节序这些函数不做转换将参数原封不动地返回。

注:这些函数名很好记h 表示 host n 表示 network l 表示 32 位长整数s 表示 16 位短整数。

举个例子:htonl函数表示将 32 位的长整数从主机字节序转换为网络字节序。

二、socket 编程接口

0. socket 常见 API

网络通信的标准方式有很多种比如基于 IP 的网络通信(它对应的通信协议家族是 AF_INET网络套接字还有原始套接字、域间套接字。有很多种类的套接字其实就是编程接口。这几种编程接口都是各自不同的体系于是就会有不同套的编程接口这样就会很麻烦因此干脆把不同套的编程接口统一为同一套编程接口也就是下面的这一套。换言之要使用不同种类的通信方式只需要改变传入的参数即可。

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

// 绑定端口号 (服务器, TCP/UDP)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 设置socket文件状态为监听状态 (服务器, TCP)
int listen(int sockfd, int backlog);

// 接受连接 (服务器, TCP)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

// 发起连接 (客户端, TCP)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

于是为了支持不同种类的通信方式struct sockaddr的结构就被设计出来了它是一种通用结构。

在这里插入图片描述

IPv4 地址使用的结构体是struct sockaddr_in我们后面经常用到它其定义为:在这里插入图片描述

socket API 的参数都用struct sockaddr *类型表示在使用时传入各种类型的struct sockaddr指针强转成struct sockaddr *即可。

这样API 内部只要取得某种struct sockaddr的首地址不需要知道具体类型就可以根据地址类型字段确定结构体中的内容。

1. socket 系统调用

socket的作用:为网络通信创建一个 socket 文件。

socket的参数:
 ① domain:指定协议家族。我们选择 AF_INET 。
 ② type:指定套接字类型。对于 TCP 应选择 SOCK_DGRAM ;对于 UDP 应选择 SOCK_STREAM 。
 ③ protocol:指定协议类型。在 TCP 和 UDP 中我们设为 0 即可。

socket的返回值:
 ① 成功返回一个 socket 文件描述符。
 ② 错误返回 -1 。

在这里插入图片描述在这里插入图片描述

2. bind 系统调用

bind的作用:将本地地址和一个 socket 文件进行绑定。

bind的参数:
 ① sockfd:传入 socket 文件描述符。
 ② addr:用于指定本端的 socket 信息。
 ③ addrlen:用于指定本端的 socket 信息的大小。

bind的返回值:
 ① 成功返回 0 。
 ② 错误返回 -1 。

在这里插入图片描述在这里插入图片描述

3. recvfrom 系统调用

recvfrom的作用:从一个 socket 文件接收数据。

recvfrom的参数:
 ① sockfd:传入 socket 文件描述符。
 ② buf:用于存放读到的数据的用户层缓冲区。
 ③ len:用户层缓冲区的大小。
 ④ flags:读的方式。我们这里默认设为 0 即可。
 ⑤ src_addr:输入输出型参数用于获取对端的 socket 信息。
 ⑥ addrlen:输入输出型参数用于获取对端的 socket 信息的大小。

recvfrom的返回值:
 ① 成功返回接收的字节数(当对端退出时返回 0。
 ② 错误返回 -1 。

在这里插入图片描述
在这里插入图片描述

4. sendto 系统调用

sendto的作用:从一个 socket 文件发送数据。

sendto的参数:
 ① sockfd:传入 socket 文件描述符。
 ② buf:用于发送数据的用户层缓冲区。
 ③ len:发送数据的长度。
 ④ flags:发送的方式。我们这里默认设为 0 即可。
 ⑤ dest_addr:目标对端的 socket 信息。
 ⑥ addrlen:目标对端的 socket 信息的大小。

sendto的返回值:
 ① 成功返回发送的字节数。
 ② 错误返回 -1 。

在这里插入图片描述
在这里插入图片描述

5. listen 系统调用

listen的作用:设置一个 socket 文件状态为监听状态允许该 socket 文件被连接。

listen的参数:
 ① sockfd:传入 socket 文件描述符。
 ② backlog:设置连接队列的最大长度。

listen的返回值:
 ① 成功返回 0 。
 ② 错误返回 -1 。

在这里插入图片描述

6. accept 系统调用

accept的作用:从一个 socket 文件接受一个连接。

accept的参数:
 ① sockfd:传入处于 listen 状态的 socket 文件描述符。
 ② addr:输入输出型参数用于获取对端的 socket 信息。
 ③ addrlen:输入输出型参数用于获取对端的 socket 信息的大小。

accept的返回值:
 ① 成功返回一个文件描述符。
 ② 错误返回 -1 。

在这里插入图片描述
在这里插入图片描述

7. connect 系统调用

connect的作用:在一个 socket 文件上向目标发起连接。

connect的参数:
 ① sockfd:传入 socket 文件描述符。
 ② addr:目标对端的 socket 信息。
 ③ addrlen:目标对端的 socket 信息的大小。

connect的返回值:
 ① 成功返回 0 。
 ② 错误返回 -1 。

在这里插入图片描述

三、简单的 UDP 网络程序

在这里插入图片描述

一个服务器必须得让客户端知道对应服务器的 socket 信息(IP + port。
一般的服务器的 port 必须是众所周知的而且不能被轻易改变

1. udp echo

程序说明:client 输入数据发送给 server server 接收数据后打印出来并返回给 client 。

下面包含两个源文件:
 ① udp_server.cc:服务端。
 ② udp_client.cc:客户端。

  • udp_server.cc:
// udp_server.cc

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

// const uint16_t port = 8080;

void Usage(std::string proc)
{
    std::cout << "Usage: \n\t" << proc << " port" << std::endl; 
}

// ./udp_server port
int main(int argc, char *argv[])
{
    if(argc != 2){
        Usage(argv[0]);  // 参数个数不对打印说明
        return -1;
    }

    uint16_t port = atoi(argv[1]);  // atoi:字符串转整型

    //1. 创建套接字打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);  // UDP -> SOCK_DGRAM
    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 = inet_addr("xxx.xxx.xx.xxx");
    // a. 需要将人识别的点分十进制的字符串风格IP地址转换为4字节整数IP
    // b. 将4字节整数IP由主机序列转换为网络序列
    // in_addr_t inet_addr(const char *cp); 能完成上面ab两个工作
    // 但是云服务器不允许用户直接bind公网IP
    // 另外实际正常编写的时候我们也不会指明IP
    
    // INADDR_ANY:不关心数据是从哪个IP上来的只要访问的是这个端口都会接收数据
    local.sin_addr.s_addr = INADDR_ANY;  // 最常用
    // 原因:
    // 如果bind的是确定的IP(主机意味着只有发到该IP主机上面的数据才会交给网络进程
    // 但是一般服务器可能有多张网卡关联多个IP
    // 我们需要的不仅仅是某个IP上面的数据我们需要的是所有发送到该主机该端口上的数据

    //服务器bind的本质是明确绑定的端口号会被严格管理
    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;  // 对端的socket
        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;  // 0 == '\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;
}
  • udp_client.cc:
// udp_client.cc

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

void Usage(std::string proc)
{
    std::cout << "Usage: \n\t" << proc << " server_ip server_port" << std::endl;
}

// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3){
        Usage(argv[0]);
        return -1;
    }

    //1. 创建套接字打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);  // UDP -> SOCK_DGRAM
    if(sock < 0){
        std::cerr << "socket error: " << errno << std::endl;
        return 1;
    }

    // 客户端不需要由用户显式地绑定
    // a. 首先客户端必须也要有ip和port
    // b. 但是客户端不需要显式地bind
    // 因为一旦显式地bind就必须明确client要和哪一个port关联
    // client指明的port有可能被占用若被占用会导致client无法使用
    // server的端口会被严格地管理跟客户端不一样
    // server要的是port必须明确而且不变但client只要有就行
    // 一般是由OS自动给用户bind
    // 当client首次对外发送数据时OS会自动bind采用的是随机端口的方式

    //b.要给谁发
    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]);
    
    //2. 使用服务
    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)-1, 0, (struct sockaddr*)&tmp, &len);
        if(cnt > 0)
        {
            // 在网络通信中只有报文大小或者说字节流中字节的个数
            // 没有C/C++字符串这样的概念(虽然我们可能经常会遇到类似的情况
            buffer[cnt] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
        else{
            //TODO
        }
    }

    return 0;
}

运行测试:在这里插入图片描述

2.加入应用逻辑 – 执行简单的 shell 命令

程序说明:client 输入 shell 命令发送给 server server 接收命令后打印出来并返回执行命令的结果给 client 。

popen函数的作用是执行传入的第一个参数 command 执行完 command 后其结果会保存到一个文件该函数会返回该文件的文件指针第二个参数 type 表示以什么方式打开这个文件。popen函数的底层原理是先fork创建出子进程再pipe实现双方通信让父进程通过文件指针拿到结果。

pclose函数的作用是关闭该文件。

下面包含两个源文件:
 ① udp_server.cc:服务端。
 ② udp_client.cc:客户端。

  • udp_server.cc:
// udp_server.cc

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

// const uint16_t port = 8080;

void Usage(std::string proc)
{
    std::cout << "Usage: \n\t" << proc << " port" << std::endl; 
}

// ./udp_server port
int main(int argc, char *argv[])
{
    if(argc != 2){
        Usage(argv[0]);
        return -1;
    }

    uint16_t port = atoi(argv[1]);  // atoi:字符串转整型

    //1. 创建套接字打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);  // UDP -> SOCK_DGRAM
    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;  // 最常用
    
    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];

    //Xshell
    while(!quit)  // 服务器不断提供服务死循环
    {
        struct sockaddr_in peer;  // 对端的socket
        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;  // 0 == '\0'可以当做一个字符串命令
            FILE *fp = popen(buffer, "r");

            std::string echo_string;
            char line[1024] = {0};  // 使用fgets从文件一行一行地读取内容
            while(fgets(line, sizeof(line), fp) != NULL){
                echo_string += line;
            }
            // if(feof(fp)){  // 判断是否读到文件的末尾EOF
            //     //读取结果完成
            // }

            pclose(fp);
            std::cout << "client# " << buffer << std::endl;

            // 根据用户输入构建一个新的返回字符串
            sendto(sock, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);
        }
        else
        {
            //TODO
        }
    }

    return 0;
}
  • udp_client.cc:
// udp_client.cc

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

void Usage(std::string proc)
{
    std::cout << "Usage: \n\t" << proc << " server_ip server_port" << std::endl;
}

// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3){
        Usage(argv[0]);
        return -1;
    }

    //1. 创建套接字打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);  // UDP -> SOCK_DGRAM
    if(sock < 0){
        std::cerr << "socket error: " << errno << std::endl;
        return 1;
    }

    //b.要给谁发
    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]);
    
    //2. 使用服务
    while(1)
    {
        // //a.数据从哪里来?
        // std::string message;
        // std::cout << "输入# ";
        // std::cin >> message;
        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)-1, 0, (struct sockaddr*)&tmp, &len);
        if(cnt > 0)
        {
            // 在网络通信中只有报文大小或者说字节流中字节的个数
            // 没有C/C++字符串这样的概念(虽然我们可能经常会遇到类似的情况
            buffer[cnt] = 0;
            std::cout << buffer << std::endl;
        }
        else{
            //TODO
        }
    }

    return 0;
}

运行测试:在这里插入图片描述

四、简单的 TCP 网络程序

程序说明:client 输入数据发送给 server server 接收数据后打印出来并返回给 client 。

下面包含两个源文件:
 ① tcp_server.cc:服务端。
 ② tcp_client.cc:客户端。

1.单进程版本

  • tcp_server.cc:
// tcp_server.cc

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

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

// ./tcp_server port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    //1.创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);  // TCP -> SOCK_STREAM
    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;

    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    //3. 因为tcp是面向连接的a.在通信前需要建立连接 b.然后才能通信
    // 一定有人主动建立连接(客户端需要服务一定有人被动接受连接(服务器提供服务
    // 我们当前写的是一个server不间断地等待客户端到来
    // 我们要不断地给客户端提供一个建立连接的功能
    // 设置套接字是listen状态本质是允许客户端连接
    const int back_log = 5;  // 先设为5这里先不解释
    if(listen(listen_sock, back_log) < 0){
        std::cerr << "listen error: " << errno << std::endl;
        return 4;
    }

    for( ; ; ){
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // accept
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_sock < 0)
        {
            continue;
        }

        std::cout << "get a new link..." << std::endl;
        
        // 单进程版本没人使用
        // 提供服务是一个死循环
        while(true)
        {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            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){  // 读到EOF表明对端退出了
                std::cout << "client quit..." << std::endl;
                break;
            }
            else{
                std::cerr << "read error" << std::endl;
                break;
            }
        }
    }

    return 0;
}
  • tcp_client.cc:
// tcp_client.cc

#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>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
}

// ./tcp_client server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string svr_ip = argv[1];
    uint16_t svr_port = (uint16_t)atoi(argv[2]);

    //1. 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);  // TCP -> SOCK_STREAM
    if(sock < 0)
    {
        std::cerr << "socket error!" << std::endl;
        return 2;
    }

    // client无需显式地bindclient -> server
    // client -> connect!
    struct sockaddr_in server;
    bzero(&server, sizeof(server));  // 将一段空间清零不推荐使用
    server.sin_family = AF_INET;
    server.sin_port = htons(svr_port);
    server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
    
    //2. 发起连接
    if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
        std::cerr << "connect server failed!" << std::endl;
        return 3;
    }

    std::cout << "connect success!" << std::endl;

    // 进行正常的业务请求
    while(true)
    {
        std::cout << "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;
}

运行测试:
在这里插入图片描述

2.多进程版本

  • tcp_server.cc:
// tcp_server.cc

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

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

void ServiceIO(int new_sock)
{
    // 提供服务是一个死循环
    while(true)
    {
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));
        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){  // 读到EOF表明对端退出了
            std::cout << "client quit..." << std::endl;
            break;
        }
        else{
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}

// ./tcp_server port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    //1.创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);  // TCP -> SOCK_STREAM
    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;

    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    //3. 设置套接字是listen状态本质是允许客户端连接
    const int back_log = 5;
    if(listen(listen_sock, back_log) < 0){
        std::cerr << "listen error: " << errno << std::endl;
        return 4;
    }

    // 在Linux中父进程忽略子进程的SIGCHLD信号子进程会自动退出释放资源
    signal(SIGCHLD, SIG_IGN);

    for( ; ; ){
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // accept
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_sock < 0)
        {
            continue;
        }
        
        // 打印对端的socket信息			
        uint16_t cli_port = ntohs(peer.sin_port);  // 网络序列转主机序列
        std::string cli_ip = inet_ntoa(peer.sin_addr);  // 作用与inet_addr函数相反

        std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port << "]# " << new_sock << std::endl;
        
        // 多进程版本
        pid_t id = fork();
        if(id < 0){
            continue;
        }
        else if(id == 0){  // 曾经被父进程打开的fd会被子进程继承
            // child       // 无论父子进程强烈建议关闭掉不需要的fd
            close(listen_sock);
            
            // 不设置信号的另一种做法
            // if(fork() > 0) exit(0);  //退出的是子进程向后走的是孙子进程父孙进程没关系

            ServiceIO(new_sock);
            close(new_sock);  //如果不关闭不需要的fd会造成fd泄漏
            exit(0);
        }
        else{
            // parent不需要等待child
            // waitpid(id, nullptr, 0);  // 阻塞式等待但几乎不阻塞因为子进程很快退出
            close(new_sock);            
        }
    }

    return 0;
}
  • tcp_client.cc:
// tcp_client.cc

#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>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
}

// ./tcp_client server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string svr_ip = argv[1];
    uint16_t svr_port = (uint16_t)atoi(argv[2]);

    //1. 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);  // TCP -> SOCK_STREAM
    if(sock < 0)
    {
        std::cerr << "socket error!" << std::endl;
        return 2;
    }

    // client无需显式地bindclient -> server
    // client -> connect!
    struct sockaddr_in server;
    bzero(&server, sizeof(server));  // 将一段空间清零不推荐使用
    server.sin_family = AF_INET;
    server.sin_port = htons(svr_port);
    server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
    
    //2. 发起连接
    if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
        std::cerr << "connect server failed!" << std::endl;
        return 3;
    }

    std::cout << "connect success!" << std::endl;

    // 进行正常的业务请求
    while(true)
    {
        std::cout << "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;
}

运行测试:
在这里插入图片描述

3.多线程版本

  • tcp_server.cc:
// tcp_server.cc

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

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

void ServiceIO(int new_sock)
{
    // 提供服务是一个死循环
    while(true)
    {
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));
        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){  // 读到EOF表明对端退出了
            std::cout << "client quit..." << std::endl;
            break;
        }
        else{
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}

void *HandlerRequest(void *args)
{
    pthread_detach(pthread_self());
    int sock = *(int*)args;
    delete (int*)args;

    ServiceIO(sock);
    close(sock);
}

// ./tcp_server port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    //1.创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);  // TCP -> SOCK_STREAM
    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;

    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    //3. 设置套接字是listen状态本质是允许客户端连接
    const int back_log = 5;
    if(listen(listen_sock, back_log) < 0){
        std::cerr << "listen error: " << errno << std::endl;
        return 4;
    }

    for( ; ; ){
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // accept
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_sock < 0)
        {
            continue;
        }
        
        // 打印对端的socket信息			
        uint16_t cli_port = ntohs(peer.sin_port);  // 网络序列转主机序列
        std::string cli_ip = inet_ntoa(peer.sin_addr);  // 作用与inet_addr函数相反
        
        std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port << "]# " << new_sock << std::endl;

        // 多线程版本
        // 曾经被主线程打开的fd新线程能看到和共享
        pthread_t tid;
        int* pram = new int(new_sock);
        pthread_create(&tid, nullptr, HandlerRequest, (void*)pram);
    }

    return 0;
}
  • tcp_client.cc:
// tcp_client.cc

#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>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
}

// ./tcp_client server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string svr_ip = argv[1];
    uint16_t svr_port = (uint16_t)atoi(argv[2]);

    //1. 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);  // TCP -> SOCK_STREAM
    if(sock < 0)
    {
        std::cerr << "socket error!" << std::endl;
        return 2;
    }

    // client无需显式地bindclient -> server
    // client -> connect!
    struct sockaddr_in server;
    bzero(&server, sizeof(server));  // 将一段空间清零不推荐使用
    server.sin_family = AF_INET;
    server.sin_port = htons(svr_port);
    server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
    
    //2. 发起连接
    if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
        std::cerr << "connect server failed!" << std::endl;
        return 3;
    }

    std::cout << "connect success!" << std::endl;

    // 进行正常的业务请求
    while(true)
    {
        std::cout << "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;
}

运行测试:
在这里插入图片描述

4.线程池版本

上面的多进程版本和多线程版本的网络程序都是有问题的:创建进程或线程无上限而且当客户端连接来了我们才给客户端创建进程或线程。

实际上最好是线程池版本。

下面包含四个文件:
 ① Task.hpp:任务的声明和定义。
 ② thread_pool.hpp:线程池的声明和定义。
 ③ tcp_server.cc:服务端。
 ④ tcp_client.cc:客户端。

  • Task.hpp:
// Task.hpp

#pragma once

#include <iostream>
#include <cstring>
#include <unistd.h>

namespace ns_task
{
    class Task
    {
    private:
        int sock_;

    public:
        Task() : sock_(-1) {}
        Task(int sock) : sock_(sock)
        {
        }

        int Run()
        {
            // 实际上不应该是长服务应该是短服务
            // 相当于一请求一响应就完了

            // // 提供服务是一个死循环
            // while(true)
            // {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t s = read(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(sock_, echo_string.c_str(), echo_string.size());
            }
            else if (s == 0)
            { // 读到EOF表明对端退出了
                std::cout << "client quit..." << std::endl;
                // break;
            }
            else
            {
                std::cerr << "read error" << std::endl;
                // break;
            }
            // }

            close(sock_);
        }

        ~Task() {}
    };
}
  • thread_pool.hpp:
// thread_pool.hpp

#pragma once

#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>

namespace ns_threadpool
{
    const int g_num = 5;

    template <class T>
    class ThreadPool
    {
    private:
        int num_;
        std::queue<T> task_queue_; // 该成员是一个临界资源

        pthread_mutex_t mtx_;
        pthread_cond_t cond_;

        static ThreadPool<T> *ins;

    private:
        // 构造函数必须得实现但是必须得私有化
        ThreadPool(int num = g_num) : num_(num)
        {
            pthread_mutex_init(&mtx_, nullptr);
            pthread_cond_init(&cond_, nullptr);
        }

        ThreadPool(const ThreadPool<T> &tp) = delete;
        ThreadPool<T> &operator=(const ThreadPool<T> &tp) = delete;

    public:
        static ThreadPool<T> *GetInstance()
        {
            static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
            if (ins == nullptr) //双判定减少锁的争用提高获取单例的效率
            {
                pthread_mutex_lock(&lock);
                // 当前单例对象还没有被创建
                if (ins == nullptr)
                {
                    ins = new ThreadPool<T>();
                    ins->InitThreadPool();
                    std::cout << "首次加载对象" << std::endl;
                }
                pthread_mutex_unlock(&lock);
            }

            return ins;
        }

        void Lock()
        {
            pthread_mutex_lock(&mtx_);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&mtx_);
        }
        void Wait()
        {
            pthread_cond_wait(&cond_, &mtx_);
        }
        void Wakeup()
        {
            pthread_cond_signal(&cond_);
        }
        bool IsEmpty()
        {
            return task_queue_.empty();
        }

    public:
        // 在类中要让线程执行类内成员方法是不可行的原因:隐含的参数this
        // 所以必须让线程执行静态方法
        static void *Routine(void *args)
        {
            pthread_detach(pthread_self());
            ThreadPool<T> *tp = (ThreadPool<T> *)args;

            while (true)
            {
                tp->Lock();
                while (tp->IsEmpty())
                {
                    // 任务队列为空
                    tp->Wait();
                }
                // 该任务队列中一定有任务了
                T t;
                tp->PopTask(&t);
                tp->Unlock();

                t.Run();
            }
        }
        void InitThreadPool()
        {
            pthread_t tid;
            for (int i = 0; i < num_; ++i)
            {
                pthread_create(&tid, nullptr, Routine, (void *)this);
            }
        }
        void PushTask(const T &in)
        {
            Lock();
            task_queue_.push(in);
            Unlock();
            Wakeup();
        }
        void PopTask(T *out)
        {
            *out = task_queue_.front();
            task_queue_.pop();
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&mtx_);
            pthread_cond_destroy(&cond_);
        }
    };

    template <class T>
    ThreadPool<T> *ThreadPool<T>::ins = nullptr;
} // namespace ns_threadpool
  • tcp_server.cc:
// tcp_server.cc

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

using namespace ns_threadpool;
using namespace ns_task;

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

// ./tcp_server port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    //1.创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);  // TCP -> SOCK_STREAM
    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;

    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    //3. 设置套接字是listen状态本质是允许客户端连接
    const int back_log = 5;
    if(listen(listen_sock, back_log) < 0){
        std::cerr << "listen error: " << errno << std::endl;
        return 4;
    }

    for( ; ; ){
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // accept
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_sock < 0)
        {
            continue;
        }
        
        // 打印对端的socket信息			
        uint16_t cli_port = ntohs(peer.sin_port);  // 网络序列转主机序列
        std::string cli_ip = inet_ntoa(peer.sin_addr);  // 作用与inet_addr函数相反
        
        std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port << "]# " << new_sock << std::endl;

        // 线程池版本(单例模式
        //1. 构建一个任务
        Task t(new_sock);
        //2. 将任务push到后端的线程池即可
        ThreadPool<Task>::GetInstance()->PushTask(t);
    }

    return 0;
}
  • tcp_client.cc:
// tcp_client.cc

#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>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
}

// ./tcp_client server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string svr_ip = argv[1];
    uint16_t svr_port = (uint16_t)atoi(argv[2]);

    //1. 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);  // TCP -> SOCK_STREAM
    if(sock < 0)
    {
        std::cerr << "socket error!" << std::endl;
        return 2;
    }

    // client无需显式地bindclient -> server
    // client -> connect!
    struct sockaddr_in server;
    bzero(&server, sizeof(server));  // 将一段空间清零不推荐使用
    server.sin_family = AF_INET;
    server.sin_port = htons(svr_port);
    server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
    
    //2. 发起连接
    if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
        std::cerr << "connect server failed!" << std::endl;
        return 3;
    }

    std::cout << "connect success!" << std::endl;

    // 进行正常的业务请求
    while(true)
    {
        std::cout << "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;
}

运行测试:
在这里插入图片描述

5. TCP 简单总结

  1. 创建 socket 的过程socket()本质是打开文件 – 仅仅有系统相关的内容。
  2. bind()struct sockaddr_in -> IP, port本质是 IP + port 和文件信息进行关联。
  3. listen()本质是设置该 socket 文件状态为监听状态允许客户端来连接。
  4. connect()本质是发起连接系统层面:就是构建一个请求报文发送过去。网络层面:发起 TCP 连接的三次握手。
  5. accept()获取新连接到应用层是以 fd 为代表的。
  6. 读/写本质就是进行网络通信但是对于用户来讲相当于在进行正常的文件读写。
  7. close()关闭文件系统层面:释放曾经申请的文件资源、连接资源等。网络层面:通知对方连接已经关闭了其实就是在进行四次挥手。

当有很多连接连上服务器时OS 中会存在大量的连接于是 OS 要管理这些已经建立好的连接。如何管理呢?先描述再组织

所谓的连接在 OS 层面上其实就是一个描述连接的文件结构体。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux

“Linux网络编程套接字” 的相关文章