【Linux系统编程】TCP通信&基于TCP实现多进程/线程并发服务器

1. TCP通信流程

TCP vs. UDP

TCP 和 UDP -> 传输层的协议

  • UDP:用户数据报协议,面向无连接,可以单播,多播,广播,面向数据报,不可靠;效率高
  • TCP:传输控制协议,面向连接的,仅支持单播传输,基于字节流,可靠的。
UDPTCP
是否创建连接无连接面向连接
是否可靠不可靠可靠的
连接的对象个数一对一、一对多、多对一、多对多支持一对一
传输的方式面向数据报面向字节流
首部开销8个字节最少20个字节
适用场景实时应用(视频会议,直播)可靠性高的应用(文件传输)

在这里插入图片描述

TCP通信的流程:

  • 服务器端(被动接受连接的角色)

    1. 创建一个用于监听的套接字

      ​ - 监听:监听有客户端的连接

      ​ - 套接字:这个套接字其实就是一个文件描述符(读:接收数据;写:发送数据)

    2. 将这个监听文件描述符和本地的 IP 和端口绑定(IP和端口就是服务器的地址信息)

      ​ - 客户端连接服务器的时候使用的就是这个 IP和端口

    3. 设置监听,监听的 fd开始工作

    4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个用于和客户端通信的套接字(fd)

    5. 通信

      ​ - 接收数据

      ​ - 发送数据

    6. 通信结束,断开连接

  • 客户端

    1. 创建一个用于通信的套接字(fd)

    2. 连接服务器,需要指定连接的服务器的 IP 和 端口

    3. 连接成功了,客户端可以直接和服务器通信

      • 接收数据

      • 发送数据

    4. 通信结束,断开连接

2. 套接字函数

#include <sys/types.h> 
#include <sys/socket.h> 
#include <arpa/inet.h> //包含了这个头文件,上面两个就可以省略 
int socket(int domain, int type, int protocol); 
	- 功能:创建一个套接字 
    - 参数: 
		- domain:协议族 
            AF_INET : ipv4 
            AF_INET6 : ipv6 
            AF_UNIX, AF_LOCAL :本地套接字通信(本地进程间通信) 
		- type:通信过程中使用的协议类型 
            SOCK_STREAM :流式协议 
            SOCK_DGRAM :报式协议 
		- protocol:具体的一个协议。
            一般写 0:
            	type = SOCK_STREAM :流式协议默认使用 TCP 
            	type = SOCK_DGRAM  :报式协议默认使用 UDP 
	- 返回值: 
            - 成功:返回文件描述符,操作的就是内核缓冲区。 
            - 失败: -1 
                	
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //socket命名 
    - 功能:绑定,将 fd 和本地的 IP + 端口 进行绑定 
    - 参数: 
        sockfd :通过 socket函数得到的文件描述符 
        addr :需要绑定的 socket地址,这个地址封装了 ip和端口号的信息 
        addrlen :第二个参数结构体占的内存大小 
            
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn 
    -功能:监听这个 socket上的连接 
    -参数: 
        sockfd :通过 socket()函数得到的文件描述符 
        backlog :未连接的和已经连接的和的最大值,
            eg. 5 (不能超过/proc/sys/net/core/somaxconn 中最大值)
            底层维护了两个队列,分别为已连接和未连接的socket连接队列
            当调用accept()后已连接的socket队列就会减一,速度很快,所以backlog值不用过大。
    -返回值:
            success, zero is returned.  
            error, -1 is returned, and errno is set appropriately.
            
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 
    -功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 
    -参数: 
        sockfd :用于监听的文件描述符 (在底层将客户端的地址信息(ip, port)读取出来)
        addr :传出参数,记录了连接成功后客户端的地址信息(ip,port) 
        addrlen :指定第二个参数的对应的内存大小 
    -返回值: 
        成功: 用于通信的文件描述符 
        失败: -1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 
    -功能:客户端连接服务器
    -参数:
        sockfd: 用于和客户端通信的文件描述符
        addr: 客户端要连接的服务器的地址信息
        addrlen: 第二个参数的内存大小
    -返回值:成功0,失败-1
ssize_t write(int fd,const void* buf, size_t count);//写数据
ssize_t read(int fd,void* buf, size_t count);//读数据

实现TCP通信 (回射服务器)

案例:实现TCP通信 服务端/客户端 并实现手动从客户端输入并从服务端返回相同内容(“回射服务器”)

(但是只能接受一个客户端通信)

// TCP通信 客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
    // 1. 创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    // 2. 连接服务器端
    struct sockaddr_in serverAddr;
    serverAddr.sin_port = htons(9999);
    serverAddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.241.128", &serverAddr.sin_addr.s_addr);
    int ret = connect(fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
    if(ret == -1)
    { 
        perror("connect");
        exit(-1);
    }
    // 3. 通信
    char recvBuf[1024] = {0};
    char data[1024] = {0};
    while(1)
    {
        // char * data = "hello, i am client";
        memset(data, 0, 1024); // 将data内容清空
        // 获取标准输入的数据
        fgets(data, 1024, stdin);
        // 发送给客户端数据
        write(fd, data, strlen(data));
        // 获取服务器端的数据
        memset(recvBuf, 0, 1024); // 将recvBuf内容清空
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1)
        {
            perror("read");
            exit(-1);
        }
        else if(len > 0) printf("recv server data: %s\n", recvBuf);
        else if(len == 0)
        {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }
    }
    // 关闭连接
    close(fd);
    return 0;
}
// TCP 通信的服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
    // 1. 创建用于监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("soclet");
        exit(-1);
    }
    // 2. 绑定 对应的IP地址和端口号port
    struct sockaddr_in saddr;
    saddr.sin_family = PF_INET;
    // inet_pton(AF_INET, "192.168.241.128", &saddr.sin_addr.s_addr);
    // 计算机有多个网卡 无线网卡、以太网卡... 
    // IP地址也都不同
    // saddr.sin_addr.s_addr = 0; 表示所有网卡都绑定 客户端连接任何IP都可以访问到主机
    saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
    saddr.sin_port = htons(9999);
    int ret = bind(lfd, (struct sockaddr *) &saddr, sizeof(saddr));
    if(ret == -1)
    {
        perror("bind");
        exit(-1);
    }
    // 3. 监听是否有客户端连接
    ret = listen(lfd, 8);
    if(ret == -1)
    {
        perror("listen");
        exit(-1);
    }
    // 4. 接收客户端连接
    struct sockaddr_in clientaddr;
    socklen_t sock_len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr *) &clientaddr, &sock_len);
    if(cfd == -1)
    {
        perror("accept");
        exit(-1);
    }
    // 5. 输出客户端信息
    char clientIP[16];
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d\n", clientIP, clientPort);
    // 5. 
    // 获取客户端的数据
    char recvBuf[1024] = {0};
    while(1)
    {
        memset(recvBuf, 0, 1024);
        int len = read(cfd, recvBuf, sizeof(recvBuf));
        if(len == -1)
        {
            perror("read");
            exit(-1);
        }
        else if(len > 0)  printf("recv client data: %s\n", recvBuf);
        else if(len == 0)
        {
            // 表示客户端断开连接
            printf("client closed...\n");
            break;
        }
        // char * data = "hello, i am server";
        // 发送给客户端数据
        write(cfd, recvBuf, strlen(recvBuf));
    }
    // 关闭文件描述符
    close(cfd);
    close(lfd);
    return 0;
}

3. TCP 三次握手***

  • TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。
  • TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在 TCP 头部。
  • TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用四次挥手来关闭一个连接。
    • 三次握手的目的:保证双方互相建立了连接

      • (为什么要三次握手而不是两次):确认客户端 和 服务端 的信息收发功能正常。
    • 三次握手发生在客户端连接的时候,当调用connect()时,底层会通过TCP协议进行三次握手。

在这里插入图片描述

  • 16 位端口号(port number):告知主机报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统自动选择的临时端口号。

  • 32 位序号(sequence number):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。

    • 假设主机 A 和主机 B 进行 TCP 通信,A 发送给 B 的第一个 TCP 报文段中,序号值被系统初始化为某个随机值 ISN(Initial Sequence Number,初始序号值)(生成规则?)。那么在该传输方向上(从 A 到 B),后续的 TCP 报文段中序号值将被系统设置成 ISN 加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如,某个 TCP 报文段传送的数据是字节流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN + 1025。另外一个传输方向(从 B 到 A)的 TCP 报文段的序号值也具有相同的含义。
  • 32 位确认号(acknowledgement number):用作对另一方发送来的 TCP 报文段的响应。其值是收到的 TCP 报文段的序号值 + 标志位长度(SYN,FIN) + 数据长度。假设主机 A 和主机 B 进行 TCP 通信,那么 A 发送出的 TCP 报文段不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段的确认号。反之,B 发送出的 TCP 报文段也同样携带自己的序号和对 A 发送来的报文段的确认序号。

    • 只有收到对方的报文段 ACK/FIN 其中一个标志位置1,自己发送时ack = 收到的 TCP 报文段的序号值 + 1

    • 只有ACK置为1,对应的ack序号才有效。

  • 4 位头部长度(head length):标识该 TCP 头部有多少个 32 bit(4 字节)。因为 4 位最大能表示 15,所以 TCP 头部最长是60 字节。

  • 6 位标志位包含如下几项:

    • URG 标志,表示紧急指针(urgent pointer)是否有效。

    • ACK 标志,表示确认号是否有效。我们称携带 ACK 标志的 TCP 报文段为确认报文段。

    • PSH 标志,提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。

    • RST 标志,表示要求对方重新建立连接。我们称携带 RST 标志的 TCP 报文段为复位报文段。

    • SYN 标志,表示请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为同步报文段。

    • FIN 标志,表示通知对方本端要关闭连接了。我们称携带 FIN 标志的 TCP 报文段为结束报文段。

    • 16 位窗口大小(window size):是 TCP 流量控制的一个手段。这里说的窗口,指的是接收通告窗口(Receiver Window,RWND)。它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。

    • 16 位校验和(TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法以校验 TCP 报文段在传输过程中是否损坏。注意,这个校验不仅包括 TCP 头部,也包括数据部分。这也是 TCP 可靠传输的一个重要保障。

    • 16 位紧急指针(urgent pointer):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。

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

  • 第一次握手:

    • 客户端将SYN标志置为1

    • 生成一个随机的32位序号seq = J

      (这个序号后面可以显示携带数据的大小 eg. seq = 1001(100) -> seq = 1101)

  • 第二次握手

    • 服务器端接收客户端的连接:ACK = 1
    • 服务器回发确认序号:ack = 客户端的序号 + 数据长度 + SYN/FIN(按一个字节来算,确保数据安全性)
    • 服务器端会向客户端发起连接请求:SYN = 1
    • 服务器会生成一个随机序号:seq = K
  • 第三次握手

    • 客户端应答服务器的连接请求:ACK = 1
    • 客户端回复收到了服务器端的数据:ack = 服务端的序号 + 数据长度 + SYN/FIN(按一个字节来算,确保数据安全性)

4. TCP 滑动窗口

  • 滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。

  • TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0时,发送方一般不能再发送数据报。

  • 滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构。

  • 窗口理解为缓冲区的大小。

  • 滑动窗口的大小会随着发送数据和接收数据而变化。

  • 通信的双方都有发送缓冲区和接收数据的缓冲区。

    • 服务器:
      • 发送缓冲区(发送缓冲区的窗口)
      • 接收缓冲区(接收缓冲区的窗口)
    • 客户端:
      • 发送缓冲区(发送缓冲区的窗口)
      • 接收缓冲区(接收缓冲区的窗口)

  • 发送方的缓冲区:
    • 白色格子:空闲空间
    • 灰色格子:数据已经被发送,但是还没有被接收。
    • 紫色格子:还没有发送出去的数据
  • 接收方的缓冲区
    • 白色格子:空闲空间
    • 紫色格子:已经接受到的数据

在这里插入图片描述

# mss:Maximum Segment Size(一条数据(报文段)的最大数据量)
# win:滑动窗口的大小  单位:字节
1. 客户端向服务器发起连接,客户端的滑动窗口是4096B,一次发送的报文段最大数据量是1460B
2. 服务器端接受连接情况,告诉客户端 服务器端的窗口大小是6144B,一次发送的报文段最大数据量是1024B
3. 第三次握手...
4-9. 客户端连续给服务器端发送了6KB的数据,每次报文段大小为1024B
10. 服务器端告诉客户端:发送的6K数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了2KB,窗口大小是2KB
11. 服务器端告诉客户端:发送的6K数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了4KB,窗口大小是4KB
12. 客户端给服务器端发送了1024B的数据
13. 第一次挥手,客户端主动请求和服务器断开连接 FIN = 1,并且给服务器端发送了1KB的数据
14. 第二次挥手,服务器端回复ACK 8194(1) 确认断开连接的请求 (2) 告诉客户端已经接受到刚才发的2KB的数据 (3) 滑动窗口大小是2KB
15-16. 通知客户端滑动窗口的大小。
17. 第三次挥手,服务器端给客户端发送FIN = 1,请求断开连接。
18. 第四次挥手,客户端确认服务器端的断开请求。

注意:

  • 第一次握手,发送方不能发送数据(不包括TCP文件头);

    第二次握手,接收方不能发送数据(只能完成三次握手后);

    第三次握手,发送方就可以发送数据了

  • 主动发送断开请求FIN的一方,在收到对方确认后,就不能再主动发送数据。但是可以再接收数据。

5. TCP 四次挥手

四次挥手:发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行4次挥手。

客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就发起。

因为在TCP连接的时候,采用三次握手建立的连接是双向的,在断开的时候也需要双向断开。

在这里插入图片描述

在这里插入图片描述

6. TCP 通信并发

要实现TCP通信服务器处理并发的任务,使用多线程/多进程解决。

思路:

  1. 一个父进程,多个子进程
  2. 父进程负责等待并接受客户端的连接
  3. 子进程:完成通信。接受一个客户端的连接,就创建一个子进程用于通信

基于多进程实现并发服务器

// 服务器端
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>

void recycleChild(int arg)
{
    while(1)
    {
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1)
        {
            // 所有的子进程都回收完了
            break;
        }
        else if(ret == 0)
        {
            // 还有子进程活着
            break;
        }
        else if(ret > 0)
        {
            // 一个子进程被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main()
{
    // 注册信号捕捉
    struct sigaction act;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recycleChild;
    sigaction(SIGCHLD, &act, NULL);
    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket");
        exit(-1);
    }
    // 绑定
    struct sockaddr_in seraddr;
    seraddr.sin_port = htons(9999);
    seraddr.sin_family = AF_INET;
    seraddr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret == -1)
    {
        perror("bind");
        exit(-1);
    }
    // 监听
    ret = listen(lfd, 128);
    if(ret == -1)
    {
        perror("listen");
        exit(-1);
    }
    // 循环等待客户端连接
    while(1)
    {
        // 接受连接
        struct sockaddr_in cliaddr;
        socklen_t len = sizeof(cliaddr);
        /*
        主进程可以考虑close(cfd), 因为linux系统默认最多只能开1024个文件,
        即使更改后也有4096的最高上限数量,我之前在进行65535个端口扫描时发现不及时close是会溢出报错的
        */
        int cfd = accept(lfd, (struct sockaddr *)&cliaddr,&len); 
        /* 
        如果某个子进程退出,那么主进程会收到信号SIGCHLD调用回调函数,触发软中断,
        当执行完回调函数后,回到accept()(之前主进程被阻塞的位置上),accept()会报错,返回EINTR错误
        */ 
        if(cfd == -1)
        {
            if(errno == EINTR) continue; // 如果返回EINTR错误,重新进行循环
            perror("accept");
            exit(-1);
        }
        // 每一个连接进来的客户端,就创建一个子进程和客户端通信
        pid_t pid = fork();
        if(pid > 0) continue;
        else if(pid == 0)
        {
            // 子进程
            // 获取客户端的信息
            char cliIP[16];
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIP, sizeof(cliIP));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is: %s, port is: %d\n", cliIP, cliPort);

            // 接收客户端发来的数据
            char recvBuf[1024] = {0};
            while(1)
            {
                int len = read(cfd, &recvBuf, sizeof(recvBuf));
                if(len == -1)
                {
                    perror("read");
                    exit(-1);
                }
                else if(len > 0)
                {
                    printf("recv clinet data: %s\n", recvBuf);
                    // strlen在计数的时候是结束符'\0'为止,但不包含结束符。
                    write(cfd, recvBuf, strlen(recvBuf) + 1); // +1 把结束符也发送出去
                }
                else if(len == 0)
                {
                    printf("client closed...\n");
                    break;
                }
            }
            close(cfd);
            exit(0);
        }
    }
    close(lfd);
    return 0;
}

客户端:同之前的TCP通信(回射服务器)

// TCP通信 客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{
    // 1. 创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    // 2. 连接服务器端
    struct sockaddr_in serverAddr;
    serverAddr.sin_port = htons(9999);
    serverAddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.241.128", &serverAddr.sin_addr.s_addr);
    int ret = connect(fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
    if(ret == -1)
    { 
        perror("connect");
        exit(-1);
    }
    // 3. 通信
    char recvBuf[1024] = {0};
    char data[1024] = {0};
    int i = 0;
    while(1)
    {
        memset(data, 0, 1024); // 将data内容清空
        // 获取标准输入的数据
        //fgets(data, 1024, stdin);
        sprintf(data, "%d\n", i++); 
        // 发送给客户端数据
        // 发送的时候不会将 \n 发送出去 因为strlen在计数的时候是结束符'\0'为止,但不包含结束符。
        // 所以write(fd, data, strlen(data) + 1); 要+1
        write(fd, data, strlen(data) + 1);
        sleep(1);
        // 获取服务器端的数据
        memset(recvBuf, 0, 1024); // 将recvBuf内容清空
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1)
        {
            perror("read");
            exit(-1);
        }
        else if(len > 0){
            printf("recv server data: %s\n", recvBuf);
        }
        else if(len == 0)
        {
            // 表示服务器端断开连接
            printf("server closed...\n");
            break;
        }
    }
    // 关闭连接
    close(fd);
    return 0;
}

注意:

  • 发送数据:发送的时候不会将 \n 发送出去,因为strlen在计数的时候是结束符’\0’为止,但不包含结束符

    所以write(fd, data, strlen(data) + 1); 要+1

  • 我们设置信号捕捉函数来捕捉SIGCHLD信号,用于子进程结束通信退出,回收子进程资源。

  • 如果某个子进程退出,那么主进程会收到信号SIGCHLD调用回调函数,触发软中断,

    当执行完回调函数后,回到accept()(之前主进程被阻塞的位置上),accept()会报错,返回EINTR错误

    解决办法:可以设置 if(errno == EINTR) continue; // 如果返回EINTR错误,重新进行循环

  • 主进程可以考虑close(cfd), 因为linux系统默认最多只能开1024个文件,

    即使更改后也有4096的最高上限数量,之前在进行65535个端口扫描时发现不及时close是会溢出报错的

基于多线程实现并发服务器

从多进程转为多线程会有很多问题:

  • 首先多进程,父子进程的内存空间关系是“读时共享,写时拷贝”;而多线程之间的内存空间关系是,内核空间中大部分资源都是共享的(除了线程ID 阻塞信号集),用户空间除了.text和栈都是共享的

这会造成如下问题:

  • 我们在pthread_create()只能向目标线程运行函数传递一个参数(void*) arg,但是我们可能会需要很多参数,包括与客户端通信的fd,线程tid,客户端的socket地址结构体等等…因此需要建立一个结构体讲这些参数封装一起传过去。

  • 我们在while()循环中创建结构体,这个结构体是创建在主线程的栈中,在子线程运行过程中如果主线程退出while()循环释放资源会造成错误。

  • 即使我们可以在堆中创建结构体(malloc()),但是我们需要手动释放资源;并且如果线程数过大,我们需要申请许多线程,所需的资源巨大。

  • 结构体变量不能直接简单赋值,需要将其对应的内部变量分别进行赋值;或者使用memcpy()函数

  • pthread_t tid 在创建结构体变量之后,调用pthread_create()之前还没有初始化

    解决方案: 直接将pinfo->tid的地址传进 pthread_create(),不另外设置

服务器端:

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <pthread.h>
struct sockInfo{
    int fd; // 通信的文件描述符
    pthread_t tid; // 线程号
    struct sockaddr_in addr; //客户端的socket地址结构体
};
struct sockInfo sockinfos[128]; // 同时最大连接线程数128
void * working(void* arg)
{
    // 和客户端通信
    // 获取客户端的信息
    struct sockInfo* pinfo = (struct sockInfo*) arg;
    char cliIP[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIP, sizeof(cliIP));
    unsigned short cliPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is: %s, port is: %d\n", cliIP, cliPort);
    // 接收客户端发来的数据
    char recvBuf[1024];
    while(1)
    {
        int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));
        if(len == -1)
        {
            perror("read");
            exit(-1);
        }
        else if(len > 0)
        {
            printf("recv clinet data: %s\n", recvBuf);
            // strlen在计数的时候是结束符'\0'为止,但不包含结束符。
            write(pinfo->fd, recvBuf, strlen(recvBuf) + 1); // +1 把结束符也发送出去
        }
        else if(len == 0)
        {
            printf("client closed...\n");
            break;
        }
    }
    close(pinfo->fd);
    pinfo->fd = -1;
    return NULL;
}
int main()
{
    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket");
        exit(-1);
    }
    // 绑定
    struct sockaddr_in seraddr;
    seraddr.sin_port = htons(9999);
    seraddr.sin_family = AF_INET;
    seraddr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret == -1)
    {
        perror("bind");
        exit(-1);
    }
    // 监听
    ret = listen(lfd, 128);
    if(ret == -1)
    {
        perror("listen");
        exit(-1);
    }
    // 初始化数据
    int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++)
    {
        bzero(&sockinfos[i], sizeof(sockinfos[i]));
        sockinfos[i].fd = -1; // -1表示该sockinfos[i]空闲 否则占用中
        sockinfos[i].tid = -1;
    }
    // 循环等待客户端连接
    // 一旦一个客户端连接进来,就创建一个子线程创建连接
    while(1)
    {
        // 接受连接
        struct sockaddr_in cliaddr;
        socklen_t len = sizeof(cliaddr);
        int cfd = accept(lfd, (struct sockaddr*) &cliaddr, &len);
        // 创建子线程
        struct sockInfo * pinfo;
        for(int i = 0; i < max; ++i)
        {
            // 从数组中找到一个可用的sockinfos[i]
            if(sockinfos[i].fd == -1)
            {
                pinfo = &sockinfos[i];
                break;
            }
            if(i == max - 1)
            {
                // 当前sockinfos[i]都被占用 需等待...
                sleep(1);
                printf("当前连接链路被占用 需等待...");
                i = -1;
            }
        }
        pinfo->fd = cfd;
        memcpy(&pinfo->addr, &cliaddr, len);
        // 创建子线程
        pthread_create(&pinfo->tid, NULL, working, pinfo);
        // 设置线程分离 自主释放资源
        pthread_detach(pinfo->tid);
    }
    close(lfd);
    return 0;
}

7. TCP 状态转换

在这里插入图片描述

注意:

  • 在第二次挥手和第三次挥手期间,服务器端(被动关闭方)是可以继续发送数据给对方,直到都发送完毕再进行第三次挥手申请断开连接。

  • 半连接队列(SYN队列),服务端收到第一次握手后,会将sock加入到这个队列中,队列内的sock都处于SYN_RCVD 状态。

  • 全连接队列(ACCEPT队列),在服务端收到第三次握手后,会将半连接队列的sock取出,放到全连接队列中。队列里的sock都处于 ESTABLISHED状态。这里面的连接,就等着服务端执行accept()后被取出了

在这里插入图片描述

  • 2MSL(Maximum Segment Lifetime)

    主动断开连接的一方, 最后进出入一个 TIME_WAIT状态, 这个状态会持续: 2msl

    • msl: 官方建议: 2分钟, 实际是30s

      当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于TIME_WAIT 状态并持续 2MSL 时间。

      这样就能够让 TCP 连接的主动关闭方在它发送的 ACK 丢失的情况下重新发送最终的 ACK。

      主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN。事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK。

  • 半关闭

    当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。

从程序的角度,可以使用 API 来控制实现半连接状态:

#include <sys/socket.h> 
int shutdown(int sockfd, int how); 
    sockfd:	需要关闭的 socket的描述符 
    how:	允许为 shutdown操作选择以下几种方式 : 
        SHUT_RD(0): 关闭 sockfd 上的读功能,此选项将不允许 sockfd进行读操作。该套接字不再接收数据,						任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。 
        SHUT_WR(1): 关闭 sockfd 的写功能,此选项将不允许 sockfd进行写操作。进程不能在对此套接字发
                    出写操作。 
        SHUT_RDWR(2):关闭 sockfd 的读写功能。相当于调用 shutdown两次:首先是以 SHUT_RD,然后以 
                    SHUT_WR。

使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。

注意:

  1. 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用进程都调用了 close,套接字将被释放。
  2. 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信(通过sfd)。但如果一个进程 close(sfd) 将不会影响到其它进程。

8. 端口复用

查看网络相关信息的命令:netstat

boyangcao@MyLinux:~$ netstat
	# 参数
		-a: 所有的socket
		-p: 显示正在使用socket的程序的名称
		-n: 直接使用IP地址,而不通过域名服务器
		-l: 正在监听的服务器socket
		-t: 使用tcp协议传输的socket
		-d: 使用udp协议传输的socket
		
boyangcao@MyLinux:~/Linux/Lesson34$ netstat -anp | grep -i 9999
# client server 都打开并处于连接状态
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        0      0 0.0.0.0:9999            0.0.0.0:*           LISTEN      4720/./server 
tcp        0      0 127.0.0.1:9999          127.0.0.1:50722     ESTABLISHED 4720/./server 
tcp        0      0 127.0.0.1:50722         127.0.0.1:9999      ESTABLISHED 4721/./client 
boyangcao@MyLinux:~/Linux/Lesson34$ netstat -anp | grep -i 9999
# 关闭 server
tcp        0      0 127.0.0.1:9999          127.0.0.1:50722     FIN_WAIT2   -             
tcp        1      0 127.0.0.1:50722         127.0.0.1:9999      CLOSE_WAIT  4721/./client 
boyangcao@MyLinux:~/Linux/Lesson34$ netstat -anp | grep -i 9999
# 关闭 client  (之前绑定的端口9999还未释放 处于TIME_WAIT状态)
tcp        0      0 127.0.0.1:9999          127.0.0.1:50722     TIME_WAIT   -  

端口复用最常用的用途是:

  • 防止服务器重启时之前绑定的端口还未释放
  • 程序突然退出而系统没有释放端口
#include <sys/types.h> 
#include <sys/socket.h> 
#include <arpa/inet.h> // 包含了上面两个头文件
// 设置套接字的属性(不仅仅能设置端口复用)
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); 
	参数:
        - sockfd: 要操作的文件描述符
        - level: 级别 - SOL_SOCKET(端口复用的级别)
        - optname: 选项的名称
            - SO_REUSEADDR: 允许重用本地地址
			- SO_REUSEPORT: 允许重用本地端口
		- optval: 端口复用的值(整型)
            - 1: 可以复用
            - 0: 不可以复用
        - optlen: optval参数的大小
端口复用设置的时机:在服务器绑定端口之前
eg.
    int optval = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
    // 绑定
    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

现在的结果:

boyangcao@MyLinux:~/Linux/Lesson34$ netstat -anp | grep -i 9999# 关闭了server后在2MSL时间内再次打开Server,端口可以复用
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        0      0 0.0.0.0:9999            0.0.0.0:*           LISTEN      4728/./server
tcp        0      0 127.0.0.1:9999          127.0.0.1:50722     TIME_WAIT   -             
tcp        0      0 127.0.0.1:50724         127.0.0.1:9999      ESTABLISHED 4731/./client
tcp        0      0 127.0.0.1:9999          127.0.0.1:50724     ESTABLISHED 4728/./server

注意:我很疑惑为什么这里客户端设置等待键盘输入信息,然后先关闭服务器,在关闭客户端就能看到服务器进程处于time_wait状态。

但是把客户端设置成一直发送消息的状态,先关闭服务端,客户端read检测到返回值为0自动关闭,就不能看到服务器对应进程处于time_wait状态??

补充:

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
	参数:
        - sockfd: 用于通信的fd
        - buf: 传出参数,接受的数据放入到此数组中
        - len: buf数组长度
        - flags: (具体看man 2 recv) 此处为 0

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Beyon.sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值