Linux基本的TCP套接字编程(详解)

2 阅读24分钟

[TOC]

Linux基本的TCP套接字编程(详解)

一、预备知识

​ 学好套接字,不单单只是学习相应的接口函数、协议概念。需要对前面进程、线程、线程池、文件描述符等一些比较重要的知识点掌握的比较扎实才能够体会到前后知识点贯穿的重要性,才能感受到套接字学习给你带来的巨大收获。涉及比较多的是:基础I/O(关于文件描述符的)、多线程、线程安全。

1.理解源IP地址和目的IP地址

​ 因特网中的每一台主机都有自己的IP地址,如果要实现A主机与B主机进行通信,A主机就必须要知道B主机的IP地址(目的IP),这样A主机才能向B主机发送数据;B主机接收到数据后,显然也需要A主机一个响应,那么B主机就必须要知道A主机的IP地址(源IP地址);当然这里只是大概的意思,等我们对套接字编程有了一定的了解,知道两个主机之间是如何通信的,再回过头来去理解这些协议的作用和更深层的理解;

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

​ 数据链路和IP中的地址,分为是MAC地址和IP地址。前者是用来识别同一链路上不同的计算机,后者是用来识别TCP/IP网络中互联的主机和路由器。在传输层中也有这样类似于地址的概念,那就是端口号。端口号是用来识别同一台计算机中进行通信的不同应用程序。因此,它也被称为程序地址。

image-20240215200039187

​ 如上图所示:一台计算机可能有多个程序。例如接受WWW服务器的Web浏览器、电邮客户端、远程登录用的ssh客户端等程序都可以同时运行。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确将数据传输。

​ 对于源IP地址和目的IP地址,就是确定了哪两台主机要通信; ​ 对于源端口号和目的端口号,就是确定了两台主机上的哪两个进行要进行通信;

3.通过IP地址、端口号、协议号进行通信识别

​ 本质上两台主机进行通信的需要IP地址、端口号和协议号的; ​ 如下图所示:1和2的通信是在两台计算机上进行的。它们的目标端口号相同,都是80。例如打开两个Web浏览器,同时访问服务器上的两个页面,就会在这个浏览器服务器之间产生类似前面的两个通信。在这种情况下必须严格区分这两个通信。因此可以源端口号加以区分。 ​ 下图中的3和1的目标端口号完全相同,但是它们各自的源IP地址不同。此外,还有一种情况上图并未列出,那就是IP地址和端口完全都一样,只是协议号(表示上层是TCP或UDP的一种编号)。这种情况下,也会认为是两个不同的通信。 ​ 因此,TCP/IP或UDP/IP通信中通常采用5个信息来标识一个通信(这个信息在xshell中可以输入netstat -n命令显示)。他们是“源IP地址”、“目标IP地址”、“协议号”、“源端口号”、“目标端口号”。只要有一项不同就被认为是其他通信。

image-20240215201326920

总的来说:

  1. IP地址最大的意义在于指导一个报文该如何进行路径选择,到哪里去就是去找目标IP地址。
  2. 端口号的意义在于唯一的标识一台机器上的唯一一个进程。
  3. IP地址+端口号=能够标识互联网中的唯一一个进程!(也就是我们接下来讲的套接字);
  4. IP地址+port(端口号)=socket(套接字)

进程ID与端口号的理解: 我们都知道所有进程都需要一个PID来进行表示,但是不是所有的进程都是网络进程,所以不是所以进程都需要端口号。同时一个进程可以绑定多个端口号(就像学生在学校可以有学号,在健身房可以有会员号),但是一个端口号不能被多个进程绑定。

4.认识TCP协议和UDP协议

1.UDP的特点及目的

​ UDP的 User Datagram Protocol 的缩写,即用户数据报协议。

​ UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务。并且它是将应用程序发来的数据在收的那一刻,立即按照原样发送到网络上的一种机制。

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

2.TCP的特点及目的

​ TCP是Transmission Control Protocol 的缩写,即传输控制协议。

​ TCP与UDP的区别相当大。它充分地实现了数据传输时的各种控制功能,可以进行丢包时重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在UDP中都没有。此外,TCP作为一种面向有连接的协议,只要在确认通信对端存在时才会发生数据,从而可以控制通信流量的浪费。

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

​ 这里目前只需要知道UDP是无连接,TCP是有连接的即可。

5.网络字节序

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

1.大端字节序和小端字节序的回顾

  • 大端模式:数据的高字节内容保存在内存的低地址处,数据的低字节内容保存在内存的高地址处。
  • 小端模式:数据的高字节内容保存在内存的高地址处,数据的低字节内容保存在内存的低地址处。

2.字节序

​ 与同一台计算机上的进程进行通信时,一般不考虑字节序。字节序是一个处理器架构特征,用于指示像整数这样的大数据类型内部的字节如何排序。但如果涉及网络通信,那就必须考虑大小端的问题,否则对端主机识别出的数据可能与发送端想要发送的数据是不一致的。 ​ TCP/IP协议栈使用大端字节序。应用程序交换格式化数据时,字节序问题就会出现。对于TCP/IP,地址用网络字节序来表示,所以应用程序有时候需要在处理器的字节序与网络字节序之间进行转换。以确保数据的一致性。 ​ 对于TCP/IP应用程序,有四个用来在处理字节序和网络字节序之间实施转换的函数。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);  //返回值:以网络字节序表示的32位整数
uint16_t htons(uint16_t hostshort); //返回值:以网络字节序表示的16位整数
uint32_t ntohl(uint32_t netlong);   //返回值:以主机字节序表示的32位整数
uint16_t ntohs(uint16_t netshort);  //返回值:以主机字节序表示的16位整数

h表示“主机”字节序,n表示“网络”字节序 l表示“长”整数,s表示“短”整数

6.地址转换函数

​ 有时候,我们需要打印出被人能理解的地址格式(如:12.0.0.1)而不是被计算机理解的地址格式(32位二进制数),那么就需要用到以下函数。 ​ 它们在ASCII字符串(这是人们偏爱使用的格式)与网络字节序的二进制值(这里存放在套接字地址结构中的值)之间转换网际地址。

#include <arpa/inet.h>
int inet_aton(const char* strptr, struct in_addr* addrptr);
in_addr_t inet_addr(const char* strptr);
char* inet_ntoa(struct in_addr addrptr);

1.inet_aton函数

​ 该函数将strptr所指C字符串转换成一个32位的网络字节序二进制值,并且通过指针addrptr来存储。若成功则返回1,否则返回0; ​ 如果addrptr指针为空,那么该函数仍然对输入的字符串执行有效检查,但不存储任何结果。

2.inet_ntoa函数

​ 该函数讲一个32位的网络字节序二进制IPV4地址转换成相应的点分十进制数串。由该函数的返回值所指向的字符串驻留在静态内存中。

3.inet_addr函数

与inet_aton一样进行相同的转换,返回值为32的网络字节序二进制值。

二、socket编程接口

1.socket常见的API

//创建套接字
int socket(int domain, int type, int protocol);
//绑定端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//监听套接字
int listen(int sockfd, int backlog);
//接受请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

1.创建套接字

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

​ 为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数(本质就是打开网络文件),指定期望通信的协议类型(使用IPV4的TCP、使用IPV6的UDP、Unix域字节流协议等)。 ​ domain参数:指明协议族,即你想要使用什么协议(IPV4、IPV6…),它是下列表格中的某个常值。该参数也往往被称为协议域。

domain说明
AF_INETIPV4协议
AF_INET6IPV6协议
AF_LOCALUnix域协议
AF_ROUTE路由套接字
AF_KEY密钥套接字

规定:我们接下来所使用的套接字所采用的协议都是AF_INET(IPV4协议) type参数:指明套接字的类型,它是下列表格中的某个常值。

type说明
SOCK_STREAM字节流套接字
SOCK_DGRAM数据报套接字
SOCK_SEQPACKET有序分组套接字
SOCK_RAW原始套接字

​ 如果你是要TCP通信的话,就要是要SOCK_STREAM作为类型,UDP就使用SOCK_DGRAM作为类型。 ​ protocol参数:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。

protocol说明
IPPROTO_TCPTCP传输协议
IPPROTO_UDPUDP传输协议
IPPROTO_SCTPSCTP传输协议

返回值说明: 套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置。

2.绑定端口号

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

bind函数是把一个协议地址赋予一个套接字。 参数说明: sockfd参数:绑定的文件是文件描述符。也就是我们创建套接字时获取到的文件描述符。 addr参数:这个参数是指向一个特定于协议的地址结构的指针。里面包含了协议族,端口号、IP地址等。 addrlen参数:是该协议的地址结构的长度。 返回值说明: 绑定成功返回0,绑定失败返回-1,同时错误码会被设置。

3.监听套接字

// 开始监听socket (TCP, 服务器)
int listen(int sockfd, int backlog);

listen函数仅由TCP服务器调用,表明服务器对外宣告它愿意接受连接请求,它做两件事:

  1. 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说将调用connect发起连接的客户端套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求。简单来说,服务器调用listen函数,就是告诉客户端是可以连接我了。
  2. 第二个参数规定了内核应该为相应的套接字排队的最大连接个数。backlog提供了一个提示,提示系统该进程要入队的未完成连接的请求数量。其实际由系统决定,对于TCP而言,默认是128。 一旦队列满了,系统就会拒绝多余的连接请求,所有backlog的值应该基于服务器期望负载和处理量来选择,其中处理量是指接受连接请求与启动服务的数量。 一旦服务器调用了listen,所用的套接字就能接受连接请求。使用accept函数获得的连接请求并建立连接。

返回值:成功返回0,失败返回-1; 本函数通常应该在调用socket和bind这两个函数之后,并在调用accept函数之前调用。

4.接受请求

// 接收请求 (TCP, 服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

​ accept函数是由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程将被投入睡眠。 ​ 如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。我们常常称它的第一个参数为监听套接字(listening socket)描述符(由socket创建,随后用作bind和listen的第一个参数的描述符),称它的返回值为已连接套接字(connected socket)描述符。区分这两个套接字非常重要。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(也就是说对于它的TCP三路握手过程以及完成)。当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。 ​ 总的来说,函数accept所返回的文件描述符是新的套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字(sockfd)具有相同的套接字类型和地址族。传给accept的原始套接字没有关联到这个连接,而是继续保持监听状态并接受其他连接请求。

5.建立连接

// 建立连接 (TCP, 客户端)
#included <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

TCP客户用connect函数来建立与TCP服务器的连接 参数说明: sockfd参数:是由socket函数返回的套接字描述符,第二个以及第三个参数分别是指向套接字地址结构的指针和该结构的大小。 在connect中指定的地址是我们想要与之通信服务器地址。如果socket没有绑定到一个地址,connect会给调用者绑定一个默认地址。 返回值说明: 若成功则为0,若出错则为-1;

2.套接字地址结构(sockaddr)

​ 从介绍的套接字函数接口来看,bind函数,accept函数和connect函数都要一个struct sockaddr的结构体指针,我们在介绍参数的时候也已经说了,这种结构是指向一个特定于协议的地址结构的指针。里面包含了协议族、端口号、IP地址等。 ​ 在网络通信的时候,其标准方式有多种,比如:IPV4套接字地址结构——struct sockaddr_in,Unix域套接字地址结构——struct sockaddr_un;前者属于网络通信,后者属于域间通信; ​ 也就是说我们的套接字接口就这么一套,但是通信方式的确有多种,你只需要给这个结构体(struct sockaddr)传输你想要的通信方式即可;其实也不难看出,这种就类似于多态,所有的通信方式都是子类,struct sockaddr就是父类,父类指向不同的子类,就使用不同的方法; ​ 我们要做的就是在使用的时进行强制类型转换即可;可能你会想到C语言中有一个指针void*,它的功能就是可以接受任意类型的指针,再进行强转也可以。但是,早期在设计的时候还没有void *这种指针,所以这种用法一直延续至今。

image-20240215231116718

​ 大多数套接字函数都需要指向套接字地址结构的指针作为参数。每个协议族都定义了它字节的套接字地址结构。这些地址结构的名字以sockaddr_开头。

image-20240215231305014

3.struct sockaddr、struct sockaddr_in、struct sockaddr_un的区别及转换

​ 由于通信方式的种类有很多,套接字接口只要一套,如果给每个通信方式都设计一套接口,单从技术的角度来说,完全是可以的。但是从使用者和学习者来讲,无疑是增加了负担。所以早期在设计的时候,就单独设计了一个通用的套接字地址结构,我们只要给这个通用的套接字地址结构传入不同的套接字地址结构,然后进行强转。在地址结构中给到我们想要通信的IP地址、端口号以及采用的协议族。

/************************通用套接字地址结构***********************************/
/* /usr/include/bits/socket.h */ /*原码所在目录,通过vim命令查看*/
/* Structure describing a generic socket address.  */
struct sockaddr{
 __SOCKADDR_COMMON (sa_);    /* Common data: address family and length.  */
 char sa_data[14];           /* Address data.  */
};
/*************************IPV4套接字地址结构**********************************/
/* /usr/include/netinet/in.h */ /*原码所在目录,通过vim命令查看*/
/* Structure describing an Internet socket address.  */
struct sockaddr_in{
 __SOCKADDR_COMMON (sin_);
 in_port_t sin_port;         /* Port number.  */
 struct in_addr sin_addr;    /* Internet address.  */
 /* Pad to size of `struct sockaddr'.  */
 unsigned char sin_zero[sizeof (struct sockaddr) -
            __SOCKADDR_COMMON_SIZE -
            sizeof (in_port_t) -
            sizeof (struct in_addr)];
};
/*************************Unix域套接字地址结构***********************************/
/* /usr/include/sys/un.h */ /*原码所在目录,通过vim命令查看*/
/* Structure describing the address of an AF_LOCAL (aka AF_UNIX) socket.  */
struct sockaddr_un{
    __SOCKADDR_COMMON (sun_);
    char sun_path[108];   /* Path name.  */
};

​ 其中3个结构里都包含了__SOCKADDR_COMMON这个宏,我们先把它的定义找到;

/* /usr/include/bits/sockaddr.h*/ /*原码所在目录,通过vim命令查看*/
/* POSIX.1g specifies this type name for the `sa_family' member.  */
typedef unsigned short int sa_family_t;
/* This macro is used to declare the initial common members
of the data types used for socket addresses, `struct sockaddr',
`struct sockaddr_in', `struct sockaddr_un', etc.  */
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
#define __SOCKADDR_COMMON_SIZE  (sizeof (unsigned short int))

​ 这三个结构的第一个字段都是一个unsigned short int类型,只不过用宏来定义了三个不同的名字,至此第一个结构就清楚了,在一般环境下(short一般为2个字节),整个结构占用16个字节,变量sa_family占用2个字节,变量sa_data保留14个字节用于保存IP地址信息。 ​ 接着我们发现第二个结构中还有in_port_t 和 struct in_addr两个类型没有定义,继续找下去吧:

/* /usr/include/bits/socket.h */ /*原码所在目录,通过vim命令查看*/
/* Type to represent a port.  */
typedef uint16_t in_port_t;
/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr{
 in_addr_t s_addr;
};

​ 这么看来sockaddr_in这个结构也不复杂,除了一开始的2个字节表示sin_family,然后是2个字节的变量sin_port表示端口,接着是4个字节的变量sin_addr表示IP地址,最后是8个字节变量sin_zero填充尾部,用来与结构sockaddr对齐。

​ 那接下来我们整理一下,为了看的清楚,部分结构使用伪代码,不能通过编译,主要是方便理解:

/*通用套接字地址结构*/
struct sockaddr{
    uint16 sa_family;           /* Common data: address family and length.  */
    char sa_data[14];           /* Address data.  */
};
/*IPV4套接字地址结构*/
struct sockaddr_in{
    uint16 sin_family;          /* Address family AF_INET */ 
    uint16 sin_port;            /* Port number.  */
    uint32 sin_addr.s_addr;     /* Internet address.  */
    unsigned char sin_zero[8];  /* Pad to size of `struct sockaddr'.  */
};
/*Unix域套接字地址结构*/
struct sockaddr_un{
    uint16 sin_family;       /* Address family AF_LOCAL */
    char sun_path[108];      /* Path name.  */
};

附图:源码结构: image-20240216144506797

三、简单的TCP网络程序

​ 基本TCP客户/服务器通信流程如下,主要为了大家能够更好的理解 image-20240216144636902

1.TCP客户端代码

#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.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);
    if(sock < 0){
        std::cerr << "socket error!" << std::endl;
        return 2;
    }
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
    server.sin_port = htons(svr_port);
    // 2.发起链接
    if(connect(sock, (struct sockaddr*)&server,sizeof(server)) < 0){
        std::cout << "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 echp# " << buffer <<std::endl;
        }
    }
    return 0;
} 

image-20240216144842770

2.TCP服务器代码

1.单进程版

TCP服务端代码如下:

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
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;
    }
    // 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;
    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
        std::cerr<< "bind error: " << errno << std::endl;
        return 3;
    }
    /*
      因为tcp是面向连接的,所以a.在通信之前,需要建立连接 b.然后才能通信
      一定有人主动建立连接(客户端,需要服务);一定有人被动接受连接(服务器,提供服务)
      我们当前写的是一个server,周而复始的不间断的等待客户端的到来
      我们要不断的给用户提供一个建立连接的功能
      设置套接字为监听(listen)状态,本质是允许用户连接的
    */
    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){
            continue;
        }
        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);
        std::cout << "get a new llink -> :[" << cli_ip << ";" << cli_port << "]:" << new_sock << 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){
                std::cout << "client quit ..." << std::endl;
                break;
            }
            else{
                std::cerr << "read error " << std::endl;
                break;
             }
        }
    }
    return 0;
}

详解: image-20240216210403886

单进程版我们一般不用

image-20240216173437552

2.多进程版

#include <iostream>           
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.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){
            std::cout << "client quit ..." << std::endl;
            break;
        }
        else{
            std::cerr << "read error " << std::endl;
            break;
        }
    }
}
int main(int argc, char* argv[]){
    if(argc != 2){
        Usage(argv[0]);
        return 1;
    }
    // 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;
    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
        std::cerr<< "bind error: " << errno << std::endl;
        return 3;
    }
    const int back_log = 5;
    if(listen(listen_sock, back_log) < 0){
        std::cerr<< "listen error" << std::endl;
        return 4;
    }
    //signal(SIGCHLD, SIG_IGN);
    for( ; ; ){
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_sock < 0){
            continue;
        }
        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);
        std::cout << "get a new llink -> :[" << cli_ip << ";" << cli_port << "]:" << new_sock << std::endl;
        pid_t id = fork();
        if(id < 0){
            continue;
        }
        else if(id == 0){
            //child
            close(listen_sock);
            if(fork() > 0) exit(0);//退出的是子进程,向后走的是孙子进程
            ServiceIo(new_sock);
            close(new_sock);
            exit(0);
        }
        else{
            //father,不需要等待
            waitpid(id, nullptr, 0);
            close(new_sock);
        }
     }
     return 0;
}

image-20240216210247111

3.多线程版

#include<iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.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){
            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);
    return 0;
}
int main(int argc, char* argv[]){
    if(argc != 2){
        Usage(argv[0]);
        return 1;
    }
    // 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;
    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
        std::cerr<< "bind error: " << errno << std::endl;
        return 3;
    }
    const int back_log = 5;
    if(listen(listen_sock, back_log) < 0){
        std::cerr<< "listen error" << std::endl;
        return 4;
    }
    //signal(SIGCHLD, SIG_IGN);
    for( ; ; ){
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_sock < 0){
            continue;
        }
        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);
        std::cout << "get a new llink -> :[" << cli_ip << ";" << cli_port << "]:" << new_sock << std::endl;
        //被主线程打开的fd,新线程是否能看到,是否共享?是
        pthread_t tid;
        int* pram = new int(new_sock);
        pthread_create(&tid, nullptr, HandlerRequest, pram);
     }
     return 0;
}

4.线程池版

#include <iostream>          
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include "thread_pool.hpp"
#include "Task.hpp"
using namespace ns_threadpool;
using namespace ns_task;
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;
    }
    // 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;
    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
        std::cerr<< "bind error: " << errno << std::endl;
        return 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){
           continue;
       }
        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);
        std::cout << "get a new llink -> :[" << cli_ip << ";" << cli_port << "]:" << new_sock << std::endl;
        // 1.构建一个任务
        Task t(new_sock);
        // 2.将任务push到后端的线程池即可
        ThreadPool<Task>::GetInstance()->PushTask(t);
    }
    return 0;
 }
Task.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <unistd.h>
using namespace std;
namespace ns_task{
    class Task{
    private:
        int sock_;
    public:
        Task():sock_(-1){}
        Task(int sock):sock_(sock){}
        void Run(){
            //提供服务 
            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){
                std::cout << "client quit ..." << std::endl;
            }
            else{
                std::cerr << "read error " << std::endl;
            }
            close(sock_);
        }
        ~Task(){}
    };
}
thread_pool.hpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>
using namespace std;
namespace ns_threadpool{
    const int g_num = 5;
    template <class T>
    class ThreadPool{
    private:
        int num_;
        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=(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();
                    cout << "首次加载对象..." << endl;
                }
                pthread_mutex_unlock(&lock);
            }
            return ins;
        }
        void Lock(){ pthread_mutex_lock(&mtx_); }
        void Unlock(){ pthread_mutex_unlock(&mtx_); }
        bool IsEmpety(){ return task_queue_.empty();}
        void Wait(){ pthread_cond_wait(&cond_, &mtx_); }
        void WakeUp(){ pthread_cond_signal(&cond_); }
    public:
        //在类中要让线程执行类内成员方法,是不可行的
        //必须让线程执行静态方法
        static void* Rountine(void* args){
            pthread_detach(pthread_self());
            ThreadPool<T>* tp = (ThreadPool<T>*)args;
            while(true){
                tp->Lock();
                while(tp->IsEmpety()){
                    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, Rountine, (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;
}    

​ 对于上面的所给的案例,多线程和线程池版本没有再去仔细的讲解,首先基本的通信过程都是一样的,只要对进程和线程相关的函数掌握以及相关的知识点的概念掌握很熟练的话,理解起来很容易。