套接字编程 --- 三

目录

1. 前置性知识

1.1. listen 系统调用

1.2. accept 系统调用

1.3. 如何通信

1.3.1.  read 系统调用 && write系统调用

1.3.2. recv 系统调用 && send 系统调用

2. TCP --- demo 

2.1. Tcp_Server.hpp (version 1)

2.2. Tcp_Server.hpp (version 2.1)

2.3. Tcp_Server.hpp (version 2.2)

2.4. Tcp_Server.hpp (version 3)

2.5. Tcp_Client.cc (version 1) 

2.6. Tcp_Server.cc (version 1) 

2.7. Tcp_Server.hpp (version 4)

2.7.1. Task.hpp

2.7.2. version 4 版本的 Tcp.Server.hpp

2.7.3. version 4.1 版本的 Tcp.Server.hpp

2.7.4.  与version 4.1版的服务端匹配的客户端

3. 补充问题


1. 前置性知识

1.1. listen 系统调用

int listen(int sockfd, int backlog);

因为TCP是面向连接的,因此当客户端和服务端正式通信的时候,首先需要先建立连接。而对于服务端来说, 它并不知道客户端什么时候会来接, 因此它是不是应该保持一种等待连接的状态呢? 因此, listen这个系统调用就是将特定套接字设置为监听状态, 使其可以接受客户端的连接请求。

  1. sockfd:要设置为监听状态的套接字描述符。
  2. backlog:定义了在等待连接队列中可以排队的最大连接数。

成功返回0, 失败返回-1,并且 error 被适当地设置。

1.2. accept 系统调用

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept 函数用于从监听套接字接受连接它会创建一个新的套接字来处理与客户端的通信。参数 sockfd 是调用 bind和 listen 后返回的套接字描述符,而参数 add r和 addrlen 用于存储客户端的地址信息。

具体来说:

  1. sockfd 是调用 socket、bind 和 listen 后返回的套接字描述符,sockfd 表示服务器正在监听的套接字
  2. addr 是一个指向sockaddr结构体的指针,其是一个输出型参数,用于存储客户端的地址信息。这个结构体应该是用来存储与服务器通信的客户端的地址信息,但在调用accept之前,addr参数通常是指向一个用于接收客户端地址信息的缓冲区的指针。
  3. addrlen是一个指向socklen_t类型的指针,用于指示addr指向的地址结构体的大小。在调用accept之前,应该将其设置为sizeof(struct sockaddr) ,可以理解为输入输出型参数(输入型参数:addr的初始值大小, 输出型参数: addr的实际大小)。

成功调用 accept 后,addr 将包含客户端的地址信息,而 addrlen 将被更新为addr指向的结构体的实际大小。如果不关心客户端的地址信息,可以将addr和addrlen设置为NULL。

需要注意的是,accept 函数在接受到一个连接之前会一直阻塞,直到有连接请求到达一旦有连接请求到达,它会返回一个新的套接字 (服务套接字),该套接字用于与客户端进行通信,而原始的监听套接字则继续用于接受其他连接请求

fd = accpept(_sock, (struct sockaddr*)&src, &len);

为了更好地理解这两个套接字 (fd、 _sock), 我们举一个故事,用以理解:

小A同学去旅游, 到了一个景点, 正在闲逛呢, 此时来了一个人, 名叫张三, 张三说, 朋友,你是来外地的吧,第一次来这里吗? 小A说, 是的。 张三说:那你可得来尝尝咱家的手艺了, 咱家做的鱼可是远近闻名,上等佳肴啊, 你要不要来尝尝, 价格也便宜。 小A一听, 逛了挺久的,也累了, 你就答应了。 然后小A就跟着张三去了馆子, 一到馆子, 张三就说,赵四,快出来,来客人了,都招乎上。于是赵四就出来了, 然后为小A提供服务。

而此时的张三呢? 张三没有进入餐馆为小A提供服务, 而是转头就走了, 去寻找下一个顾客了。

于是张三又遇到了小B同学, 就问小B,饿不饿,想不想吃点饭,说咱家的特色菜非常好吃等等, 但是小B说, 我不饿, 我还想在逛一逛再去吃, 那么此时,这个张三怎么做呢? 难道说,张三死乞白赖的一直跟着小B? 答案是,当然不会,正常情况下, 张三一听小B不想吃,就会说你要想吃的话就来咱家,咱随时欢迎你,然后就走了,就去找下一个顾客了。

那么我们就可以通过上面的例子就可以理解这两个套接字的差别呢?

fd (赵四等提供服务的服务员) --- 服务套接字, _sock (张三  --- 获取连接请求)--- 监听套接字。

而 _sock 我们一般称之为 listen_sock, 监听套接字,它的主要功能就是获取连接请求,如果有连接请求,我们再去为这个连接请求建立连接

而这里的 fd 。 我们称之为 service_sock, 服务套接字,即服务端是通过这个套接字和客户端进行交互的。

如果创建连接失败,那么可以处理下一个连接请求或者根据具体情况进行错误处理。

如果创建连接成功,就可以通过返回的新套接字 (服务套接字) 与客户端进行通信,而原始的监听套接字则可以继续接受其他连接请求。

1.3. 如何通信

上面我们已经解决了客户端和服务端如何建立连接的问题, 那么当客户端和服务端连接成功后, 客户端和服务端如何传输数据呢?

难道还是像UDP一样, 用 recvfrom 吗? 并不是, recvfrom 是专门用UDP协议的, 是面向数据报的, 而TCP是面向字节流的。 

1.3.1.  read 系统调用 && write系统调用

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

在TCP协议中, 当服务端和客户端建立连接后, 我们可以直接使用 read 从特定套接字读取数据,也可以用 write 向特定套接字写数据,因为套接字是和文件描述符强关联的。

1.3.2. recv 系统调用 && send 系统调用

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

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

recv 和 send 这两个函数在TCP协议中通常用于进行流式套接字的数据传输

send 基于TCP向目标套接字发送消息。前三个参数和返回值和 write 一摸一样。flags == 0 , 代表阻塞式的发送消息 (缓冲区为满会被阻塞)。

recv 基于TCP从目标套接字获取消息。前三个参数和返回值和 read一摸一样。flags == 0 , 代表阻塞式的读取 (缓冲区为空会被阻塞)。

了解:

flags会有一些其他选项:

MSG_DONTWAIT: 表示以非阻塞方式进行发送或接收操作。如果设置了这个标志,函数将立即返回,而不会等待缓冲区可用或满。
MSG_NOSIGNAL:仅适用于send函数。表示在发送数据时不产生SIGPIPE信号,而是返回错误。这在处理已关闭的连接时很有用。
MSG_OOB:表示处理带外数据(Out-of-Band data)。TCP协议支持通过特殊的通道发送带外数据,这些数据可以在普通数据之外传输。使用此标志表示操作带外数据。
MSG_PEEK:表示查看套接字接收缓冲区的数据,但不移除数据。这可以用于检查接收缓冲区中的数据而不实际读取它。
MSG_WAITALL:表示在recv函数中,如果请求的字节数没有全部可用,那么将一直等待,直到请求的字节数全部可用为止。

2. TCP --- demo 

2.1. Tcp_Server.hpp (version 1)

服务端:单进程 (单执行流) 版本。

#ifndef __TCP_SERVER_HPP_
#define __TCP_SERVER_HPP_

#include <iostream>
#include <string>
#include <cstring>

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

#include <unistd.h>

#include "Log.hpp"

#define SERVER_BUFFER 1024

namespace Xq
{
  const static int backlog = 20;
  class tcp_server
  {
  public:
    tcp_server(uint16_t port, const std::string& ip = "")
    :_ip(ip), _port(port), _listen_sock(-1)
    {}

    void init_server()
    {
      // step 1: create socket
      // AF_INET, 表示IPV4互联网协议
      // SOCK_STREAM 表示这是面向字节流的
      // 此时就表示创建一个基于IPV4地址族、面向字节流的套接字
      _listen_sock = socket(AF_INET, SOCK_STREAM, 0);
      if(_listen_sock == -1)
      {
        LogMessage(FATAL, "%s,%d\n", strerror(errno));
        exit(1);
      }

      LogMessage(DEBUG, "socket success, return listen sock: %d\n", _listen_sock);

      // step 2: bind socket
      struct sockaddr_in server;
      server.sin_family = AF_INET;
      server.sin_port = htons(_port);
      // 服务端采用任意地址
      server.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

      if(-1 == bind(_listen_sock, reinterpret_cast<const struct sockaddr*>(&server), sizeof server))
      {
        LogMessage(FATAL, "%s,%d\n", strerror(errno));
        exit(2);
      }

      // 上面的过程和UDP几乎没有任何区别,
      // 无非UDP是面向数据报 --- SOCK_DGRAM
      // 而TCP是面向字节流的 --- SOCK_STREAM
      

      // step 3: set listen socket
      // TCP是面向连接的, 因此客户端和服务端首先要建立连接
      // 而对于服务端来说, 它并不知道客户端会在什么时候发起连接请求
      // 因此服务端是不是应该保持一种等待获取客户端连接请求的状态呢?
      // 答案是的, 因此listen这个系统调用就是将特定套接字设置为监听状态,
      // 获取客户端发起的连接请求。
      // backlog: 这个数字在这里暂时不解释, 后面解释
      // 一般情况下, 这个值不能太大, 也不能太小
      if(listen(_listen_sock, backlog) == -1)
      {
        LogMessage(FATAL, "%s,%d\n", strerror(errno));
        exit(3);
      }
      // server init done
      LogMessage(DEBUG, "server init success\n");
    }

    void start() 
    {
      // 服务器永不退出
      while(true)
      {
        // step 4: accept
        // accept用于接收客服端发送的连接请求
        // 那么客户端向服务端发送连接请求, 
        // 服务端需不需要知道客户端的地址信息呢?
        // 显然需要知道, 因此accept的后两个参数
        // 就是发起请求方的地址信息
        struct sockaddr_in client; // 输出型参数
        bzero(&client, sizeof client);
        socklen_t client_len = sizeof client; // 输入输出型参数
        
        // 另外, 如果服务端没有收到连接请求
        // 那么服务端会在accept内部阻塞
        // 如果有连接请求, 那么我就处理
        // 连接成功, 返回一个套接字, 服务套接字
        // 连接失败, 返回 -1;
        
        int server_sock = accept(_listen_sock, reinterpret_cast<struct sockaddr*>(&client), &client_len);
        if(server_sock == -1)
        {
          LogMessage(FATAL, "%s,%d\n", strerror(errno));
          exit(4);
        }

        LogMessage(DEBUG, "accept success, return server_sock: %d\n", server_sock);

        // 走到这里, 说明连接成功
        // 就可以进行通信了
        
        // 交互数据, 需要服务套接字, 客户端地址信息
        communicate_data(server_sock, client);

      }
    }

    // 简单的echo服务器: 将客户端发送的数据返回给客户端
    static void communicate_data(int server_sock, const struct sockaddr_in& client)
    {
      // 提取客户端的ip, 网络字节序 -> 主机序列
      uint16_t client_port = ntohs(client.sin_port);
      // 提取客户端的port, 网络字节序 -> 主机序列
      std::string client_ip = inet_ntoa(client.sin_addr);
      
      char buffer[SERVER_BUFFER] = {0};

      while(true)
      {
        // read  --- 读取客户端数据
        ssize_t read_size = read(server_sock, buffer, SERVER_BUFFER - 1);
        if(strcmp(buffer, "quit\n") == 0)
          break;
        if(read_size > 0)
        {
          LogMessage(DEBUG, "read client data success, data size: %d\n", read_size);
          buffer[read_size] = 0;
          printf("[%s][%d]: %s\n", client_ip.c_str(), client_port, buffer);
        }
        else if(read_size == 0)
        {
          // 代表客户端关闭连接了
          LogMessage(NORMAL, "client switch off connect, me too\n");
          break;
        }
        else
        {
          LogMessage(ERROR, "%d,%s\n", strerror(errno));
          exit(5);
        }

        // write --- 向客户端写数据
        write(server_sock, buffer, sizeof buffer);
      }
    }
    ~tcp_server() {}

  private:
    std::string _ip;
    uint16_t _port;
    int _listen_sock;
  };
}

#endif

由于我们目前没有写客户端, 因此我们可以用 telnet 工具,远程连接我们的服务器, 现象如下:

telnet 向服务端发送消息, 服务端将消息返回给telnet。

如果要退出 telnet 的连接, 那么先 Ctrl + ], 然后 telnet 会自动弹出 telnet>, 此时我们再输入quit, 那么 telent 就会退出,此时连接也会中断。 

 可是这样会存在问题, 现象如下:

上面的窗口先进行连接,连接成功。 而下面的窗口此时却没有成功连接,这是为什么呢?

原因是因为:我们上面的服务端是单进程 (单执行流),当服务端在执行 communicate_data 时, 由于 communicate_data 是一个死循环, 此时服务端就一直在和上面的 telnet 进程交互数据, 因此此时下面的 telnet 进程的连接请求就被阻塞了。

因此,单进程 (单执行流) 版本是明显有缺陷的, 因此我们想将它更改为多进程版本。

2.2. Tcp_Server.hpp (version 2.1)

服务端:多进程版本。

思路: 当 accept 获取连接请求,并成功连接后,创建子进程来处理客户端连接 (子进程通过服务套接字和客户端交互),通信结束, 子进程退出。  父进程继续使用监听套接字获取客户端请求,并重复上述过程。

不过,在编码之前,我们要提出几个问题:

问题一: 创建子进程, 让子进程为新的连接提供服务, 那么子进程能不能获得父进程曾经打开的文件描述符呢? 

答案: 当然可以, 父进程创建子进程时,子进程会以父进程为模板创建相应的内核数据结构,而我们以前学习过, 进程的PCB里面有一个成员为 struct files_struct* fs,其指向一张表,这张表里有一个指针数组,类型为 struct file* fd_array, 而文件描述符就是这个数组的下标,因此, 当父进程创建子进程时, 子进程也会以写实拷贝的方式获得这张表,这意味着子进程和父进程最初共享相同的文件描述符表,即会获得相应的文件描述符。

问题二:多进程版的服务端本质上想为多个客户端提供服务,换言之, 会有多个子进程被创建,而子进程退出会有僵尸问题, 如何处理子进程的僵尸问题呢?

方案一: 以非阻塞式的 waitpid 进行等待子进程退出, 可是这种方案太麻烦, 因为我们还要记录每个进程的PID, 不太好。

方案二: 对SIGCHLD信号进行自定义捕捉 (signal函数),将动作设置为 SIG_IGN。子进程退出时,内核会立即回收子进程的资源,而不会留下僵尸进程。

方案三: 创建子进程之后,在创建子进程 (在这里称之为孙子进程,方便描述),孙子进程区处理客户端连接, 并终止子进程,父进程等待子进程退出, 此时的孙子进程就会成为孤儿进程, 被操作系统领养, 因此我们不用过多处理孙子问题的僵尸问题。

问题三:子进程或者孙子进程是为客户端提供服务的, 那么有没有必要需要知道监听套接字呢?同理, 父进程是为了获取客户端连接请求的, 那么需不需要知道服务套接字呢?

答案是:不需要, 对于子进程或者孙子进程而言,只需要知道服务套接字即可。而对于父进程而言,只需要知道监听套接字即可 (获取连接请求)。 因此我们可以关闭进程所不需要的套接字, 对于前者,我们关闭监听套接字; 对于后者,我们关闭服务套接字。

最后一个问题:如果父进程, 关闭了服务套接字, 那么影不影响子进程呢?

答案是:当然不影响咯, 因为进程具有独立性 (独立的地址空间、文件描述符表等等), 会以写实拷贝的方案保证自己的独立性。

有了上面的理解, 这就好处理了, 服务端的多进程版本如下:

#ifndef __TCP_SERVER_HPP_
#define __TCP_SERVER_HPP_

#include <iostream>
#include <string>
#include <cstring>

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

#include <unistd.h>
#include <signal.h>

#include "Log.hpp"

#define SERVER_BUFFER 1024

namespace Xq
{
  const static int backlog = 20;
  class tcp_server
  {
  public:
    tcp_server(uint16_t port, const std::string& ip = "")
    :_ip(ip), _port(port), _listen_sock(-1)
    {}

    void init_server()
    {
      // step 1: create socket
      // AF_INET, 表示IPV4互联网协议
      // SOCK_STREAM 表示这是面向字节流的
      // 此时就表示创建一个基于IPV4地址族、面向字节流的套接字
      _listen_sock = socket(AF_INET, SOCK_STREAM, 0);
      if(_listen_sock == -1)
      {
        LogMessage(FATAL, "%s,%d\n", strerror(errno));
        exit(1);
      }

      LogMessage(DEBUG, "socket success, return listen sock: %d\n", _listen_sock);

      // step 2: bind socket
      struct sockaddr_in server;
      server.sin_family = AF_INET;
      server.sin_port = htons(_port);
      // 服务端采用任意地址
      server.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

      if(-1 == bind(_listen_sock, reinterpret_cast<const struct sockaddr*>(&server), sizeof server))
      {
        LogMessage(FATAL, "%s,%d\n", strerror(errno));
        exit(2);
      }

      // 上面的过程和UDP几乎没有任何区别,
      // 无非UDP是面向数据报 --- SOCK_DGRAM
      // 而TCP是面向字节流的 --- SOCK_STREAM
      
      // step 3: set listen socket
      // TCP是面向连接的, 因此客户端和服务端首先要建立连接
      // 而对于服务端来说, 它并不知道客户端会在什么时候发起连接请求
      // 因此服务端是不是应该保持一种等待获取客户端连接请求的状态呢?
      // 答案是的, 因此listen这个系统调用就是将特定套接字设置为监听状态,
      // 获取客户端发起的连接请求。
      // backlog: 这个数字在这里暂时不解释, 后面解释
      // 一般情况下, 这个值不能太大, 也不能太小
      if(listen(_listen_sock, backlog) == -1)
      {
        LogMessage(FATAL, "%s,%d\n", strerror(errno));
        exit(3);
      }
      // server init done
      LogMessage(DEBUG, "server init success\n");
    }

    void start() 
    {
      // 子进程退出, 自动被操作系统回收, 避免僵尸问题
      signal(SIGCHLD, SIG_IGN);
      // 服务器永不退出
      while(true)
      {
        // step 4: accept
        // accept用于接收客服端发送的连接请求
        // 那么客户端向服务端发送连接请求, 
        // 服务端需不需要知道客户端的地址信息呢?
        // 显然需要知道, 因此accept的后两个参数
        // 就是发起请求方的地址信息
        struct sockaddr_in client; // 输出型参数
        bzero(&client, sizeof client);
        socklen_t client_len = sizeof client; // 输入输出型参数
        
        // 另外, 如果服务端没有收到连接请求
        // 那么服务端会在accept内部阻塞
        // 如果有连接请求, 那么我就处理
        // 连接成功, 返回一个套接字, 服务套接字
        // 连接失败, 返回 -1;
        
        int server_sock = accept(_listen_sock, reinterpret_cast<struct sockaddr*>(&client), &client_len);
        if(server_sock == -1)
        {
          LogMessage(FATAL, "%s,%d\n", strerror(errno));
          exit(4);
        }

        LogMessage(DEBUG, "accept success, return server_sock: %d\n", server_sock);

        // 走到这里, 说明连接成功
        // 就可以进行通信了
        
        // 我们不想让父进程自己去处理客户端连接
        // 而是通过父进程创建的子进程去处理客户端连接
        
        pid_t id = fork();
        if(id == -1)
        {
          LogMessage(FATAL, "%s,%d\n", strerror(errno));
          exit(6);
        }

        if(id == 0)
        {
          LogMessage(DEBUG, "create child process success, child id: %d\n", getpid());
          // child process
          // 子进程不需要监听套接字
          close(_listen_sock);
          // 处理客户端连接
          communicate_data(server_sock, client);
          // 处理完后, 子进程退出
          exit(0);
        }
        // 父进程不需要知道服务套接字
        close(server_sock);
        // 父进程继续去监听客户端请求, 并对客户端请求进行连接
        // 因此,当新的客户端请求过来时, 父进程不会被阻塞
      }
    }

    // 简单的echo服务器: 将客户端发送的数据返回给客户端
    static void communicate_data(int server_sock, const struct sockaddr_in& client)
    {
      // 提取客户端的ip, 网络字节序 -> 主机序列
      uint16_t client_port = ntohs(client.sin_port);
      // 提取客户端的port, 网络字节序 -> 主机序列
      std::string client_ip = inet_ntoa(client.sin_addr);
      
      char buffer[SERVER_BUFFER] = {0};

      while(true)
      {
        // read  --- 读取客户端数据
        ssize_t read_size = read(server_sock, buffer, SERVER_BUFFER - 1);

        if(read_size > 0)
        {
          LogMessage(DEBUG, "read client data success, data size: %d\n", read_size);
          buffer[read_size] = 0;
          printf("[%s][%d]: %s\n", client_ip.c_str(), client_port, buffer);
        }
        else if(read_size == 0)
        {
          // 代表客户端关闭连接了
          LogMessage(NORMAL, "client switch off connect, me too\n");
          break;
        }
        else
        {
          LogMessage(ERROR, "%d,%s\n", strerror(errno));
          exit(5);
        }

        // write --- 向客户端写数据
        write(server_sock, buffer, sizeof buffer);
      }
    }
    ~tcp_server() {}

  private:
    std::string _ip;
    uint16_t _port;
    int _listen_sock;
  };
}

#endif

2.3. Tcp_Server.hpp (version 2.2)

上面我们是通过 signal 函数对 SIGCHLD 信号进行显式的忽略 (SIG_IGN),解决了子进程的僵尸问题, 可是如果我非要让你通过 waitpid 来解决僵尸问题, 并且这个 waitpid 是阻塞式的等待子进程退出, 且我们要求要满足多个客户端访问 (父进程还不能被阻塞住), 该如何处理?

由于大部分对吗都和 version 2.1 的一样,我们只是更改处理僵尸问题的逻辑, 因此大部分相同的代码我省略,代码如下:

namespace Xq
{
  class tcp_server
  {
  public:
    tcp_server(uint16_t port, const std::string& ip = "")
    :_ip(ip), _port(port), _listen_sock(-1){}

    void init_server() { /*省略*/}

    void start() 
    {
      // 子进程退出, 自动被操作系统回收, 避免僵尸问题
      signal(SIGCHLD, SIG_IGN);
      // 服务器永不退出
      while(true)
      {
        // accept 过程省略
        // 走到这里, 说明连接成功
        // 就可以进行通信了
      
        // 我们不想让父进程自己去处理客户端连接
        // 而是通过父进程创建的子进程的子进程 (也就是孙子进程) 去处理客户端连接
        pid_t id = fork();
        if(id == 0)
        {
          // 子进程不需要监听套接字
          close(_listen_sock);
          // 子进程
          if(fork() > 0)
          {
            // 子进程直接退出, 让孙子进程成为孤儿进程
            exit(0);
          }
          // 孙子进程处理客户端连接
          communicate_data(server_sock, client);
          // 由于子进程已经退出, 那么这个孙子进程就成为了一个孤儿进程, 被OS领养
          // 因此当它退出时, 会自动被操作系统回收, 解决了僵尸问题
          exit(0);
        }
        // 父进程不需要服务套接字
        close(server_sock);
        // 子进程一退出, 父进程就会回收其资源, 解决了僵尸问题
        // 因为对于父进程而言, 子进程几乎是一瞬间退出的
        // 因此, 父进程不会在这里一直阻塞
        // 也就会处理其他客户端的连接请求
        waitpid(id, nullptr, 0);
      }
    }

    // 简单的echo服务器: 将客户端发送的数据返回给客户端
    static void communicate_data(int server_sock, const struct sockaddr_in& client)
    { /*省略*/ }

    ~tcp_server() {}
  private:
    std::string _ip;
    uint16_t _port;
    int _listen_sock;
  };
}

#endif

2.4. Tcp_Server.hpp (version 3)

可是,我们知道,创建一个进程成本是比较高的,例如,我们要创建进程的PCB、地址空间、页表、维护各种映射关系,调度关系等等,而我们是学习过线程的,线程和进程相比,线程就更轻量化,创建和删除的成本更低, 维护成本也更低,那么我们能不能实现一个多线程版本呢?  当然可以。

namespace Xq
{
  // 线程所需要的参数
  class data_t
  {
  public:
    int _server_sock;
    struct sockaddr_in _client_addr;
  };

  class tcp_server
  {
  private:

    // 线程的回调
    static void* start_routine(void* arg)
    {
      // 这里有两个问题: 
      
      // 问题一:  线程执行完如何处理呢?
      // 难道让主线程(进程) 阻塞等待(pthread_join) 吗
      // 很明显, 我们不能让主线程进行阻塞等待
      // 因为我们需要进程一直accept客户端请求
      // 那么如何处理? 
      // 我们可以用 pthread_detach 线程分离
      // 让线程执行完后自动释放其资源, 避免了内存泄漏
      pthread_detach(pthread_self());

      // 第二个问题: 对于新线程而言, 需不需要关闭特定的文件描述符呢?
      // 不需要, 因为进程是承担资源的基本实体, 而线程的资源是依赖于进程的.
      // 因此这里的文件描述符不能关,因为它是属于这个进程的。
      // 不是属于你这个线程的,你只是和这个进程共享罢了。
      
      // 解决了上面的问题, 我们就可以处理客户端连接了
      // 我们不是有一个 communicate_data 函数吗?
      // 我们只需要调用它就可以处理客户端连接了
      // 而communicate_data 需要服务套接字, 需要客户端的地址信息
      data_t* Tdata = static_cast<data_t*>(arg);
      communicate_data(Tdata->_server_sock, Tdata->_client_addr);
      // 新线程执行完客户端连接, 就关闭掉服务套接字
      close(Tdata->_server_sock);
      delete Tdata;
      return nullptr;
    }
  public:
    tcp_server(uint16_t port, const std::string& ip = "")
    :_ip(ip), _port(port), _listen_sock(-1)  {}

    void init_server() { /*省略*/ }

    void start() 
    {
        // 省略...
        // 走到这里, 说明连接成功
        // 就可以进行通信了
        
        // 我们采用多线程的方案
        // 让新线程处理客户端连接
        pthread_t tid;
        data_t* Tdata = new data_t;
        Tdata->_server_sock = server_sock;
        Tdata->_client_addr = client;
        pthread_create(&tid, nullptr, start_routine, static_cast<void*>(Tdata));
      }
    }

    // 简单的echo服务器: 将客户端发送的数据返回给客户端
    static void communicate_data(int server_sock, const struct sockaddr_in& client)
    { /*省略*/ }

    ~tcp_server() {}

  private:
    std::string _ip;
    uint16_t _port;
    int _listen_sock;
  };
}
#endif

2.5. Tcp_Client.cc (version 1) 

上面版本相应的客户端,用以测试。

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

#include <cstring>

const int CLIENT_BUFFER = 1024;

void Usage(void)
{
  printf("please usa ./tcp_client server_ip  server_port\n");
  exit(1);
}

int main(int arg, char** argv)
{
  if(arg != 3)
  {
    Usage();
  }

  // step 1: create socket
  int sock = socket(AF_INET, SOCK_STREAM, 0);
  if(sock == -1)
  {
    LogMessage(FATAL, "%s,%d\n",strerror(errno));
    exit(2);
  }

  // 获取服务端port, 并将其转为网络字节序
  int server_port = htons(atoi(argv[2]));
  // 获取服务端IP, 并将其转为网络字节序
  uint32_t server_ip = inet_addr(argv[1]);

  // 填充服务端地址信息
  struct sockaddr_in server;
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = server_ip;
  server.sin_port = server_port;
  socklen_t server_len = sizeof(server);

  // 客户端不需要显示bind
  // 当然也不需要listen, 因为客户端是向服务端发起连接的
  // 自然也不需要accept
  
  // 那么客户端需要做什么呢?
  // 客户端最需要的是连接别人的能力, 如何连接呢?
  if(connect(sock, reinterpret_cast<const struct sockaddr*>(&server), server_len) == -1 ) 
  {
    LogMessage(FATAL, "client connect failed\n");
    exit(3);
  }

  LogMessage(DEBUG, "client connect success\n");

  // 如果connect成功代表着该套接字(sock)
  // 与这个地址信息强关联的进程(服务端进程)建立了连接
  // 此时操作系统就会自动为客户端绑定
  // 接下来, 客户端就可以和服务端交互数据了
  
  while(true)
  {
    std::cout << "请输入信息: " << std::endl;
    std::string message;
    getline(std::cin, message);

    if(message == "quit")
    {
      LogMessage(DEBUG, "client quit\n");
      break;
    }

    // 客户端向服务端发送数据 --- send
    ssize_t real_send_size = send(sock, message.c_str(), message.size(), 0);
    if(real_send_size > 0)
    {
      LogMessage(DEBUG, "send success\n");
    }
    else if(real_send_size == 0)
    {
      LogMessage(WARNING, "server disconnect\n");
      break;
    }
    else
    {
      LogMessage(ERROR, "send failed\n");
      break;
    }
    // 客户端收数据--- recv
    char buffer[CLIENT_BUFFER] = {0};
    ssize_t real_recv_size = recv(sock, buffer, CLIENT_BUFFER - 1, 0);
    if(real_recv_size > 0)
    {
      buffer[real_recv_size] = 0;
      printf("server: %s\n", buffer);
    }
    else
    {
      LogMessage(ERROR, "recv error\n");
      break;
    }
  }
  return 0;
}

2.6. Tcp_Server.cc (version 1) 

上面版本相应的服务端,用以测试。

#include <memory>
#include <iostream>
#include "Tcp_Server.hpp"

void Usage(void)
{
  printf("please use ./tcp_server port\n");
  exit(6);
}

int main(int arg, char* argv[])
{
  if(arg != 2)
  {
    Usage();
  }
  uint16_t port = atoi(argv[1]);
  std::unique_ptr<Xq::tcp_server> server(new Xq::tcp_server(port));
  server->init_server();
  server->start();
  
  return 0;
}

2.7. Tcp_Server.hpp (version 4)

可是,如果客户端请求很多, 难道每来一个客户端请求我才去创建一个线程吗,然后线程执行完,就销毁这个线程,这样是不是会导致频繁的创建和销毁?这样虽然可以,但是效率有点低, 因此为了提高效率以及减少线程创建和销毁的开销,我们可不可以提前创建一批线程, 当客户端请求来的时候, 我在去这批线程找一个线程让它帮助我处理这个客户端请求呢? 换言之,我们可不可以实现一个线程池版的服务端呢

当然是可以的, 我们以前不是实现过线程池 (具体我们选择单例模式的线程池) 吗? 因此,我们就将线程池引进来。

我们需要,Date.hpp、LockGuard.hpp、Log.hpp、Thread.hpp、Task.hpp、ThreadPool.hpp,前四个没有变化 (和以前进程池一样,如果想要再看看,请看下面链接),我们重点做出更改的是 Task.hpp, 而ThreadPool.hpp 也没有什么大改变,我们只是在原先的 run_all_thread函数中又调用了pthread_detach,避免了主线程等待新线程,因此 ThreadPool.hpp 也不再展示了。

多线程 --- [ 线程池、懒汉引发的线程安全问题、其他常见的锁 ]-CSDN博客

2.7.1. Task.hpp

#ifndef __TASK_HPP_
#define __TASK_HPP_

#include <iostream>
#include <functional>
#include <string>
#include "Log.hpp"

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

namespace Xq
{
  //typedef std::function<void (int, const struct sockaddr_in&, const std::string& ) > func_t;
  using func_t = std::function<void (int, const struct sockaddr_in&, const std::string& ) >;
  class Task
  {
  public:
    // 提供一个默认构造
    Task()  {}
    Task(int server_sock, const struct sockaddr_in& client_addr, func_t func) 
      :_server_sock(server_sock)
       ,_client_addr(client_addr)
       ,_func(func)
    {}
    void operator()(const std::string& name)
    {
      // 这里的_func就是线程池中的线程处理客户端连接
      _func(_server_sock, _client_addr, name);
      // _func 调用完, 我们就可以关闭这个服务套接字
      close(_server_sock);
      LogMessage(DEBUG, "%s close server_sock: %d\n", name.c_str(),_server_sock);
    }

  private:
    int _server_sock;
    struct sockaddr_in _client_addr;
    func_t _func;
  };
}
#endif

2.7.2. version 4 版本的 Tcp.Server.hpp

#ifndef __TCP_SERVER_HPP_
#define __TCP_SERVER_HPP_

#include <iostream>
#include <string>
#include <cstring>

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

#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include "./thread_pool/Log.hpp"
#include "./thread_pool/ThreadPool.hpp"
#include "./thread_pool/Task.hpp"

#define SERVER_BUFFER 1024

namespace Xq
{
  const static int backlog = 20;

  class tcp_server
  {
  private:
  public:
    tcp_server(uint16_t port, const std::string& ip = "")
    :_ip(ip), _port(port), _listen_sock(-1)
    {
      // 获得线程池唯一实例
      _only_thread_pool = Xq::thread_pool<Xq::Task>::get_ptr_only_thread_pool(10);
    }

    void init_server()  { /*省略*/ }

    void start() 
    {
      // 我们采用线程池的方案
      // 让线程池去处理客户端连接
      // 线程池初始化后, 启动线程
      _only_thread_pool->run_all_thread();

      // 子进程退出, 自动被操作系统回收, 避免僵尸问题
      signal(SIGCHLD, SIG_IGN);
      // 服务器永不退出
      while(true)
      {
        // step 4: accept
        // accept用于接收客服端发送的连接请求
        // 那么客户端向服务端发送连接请求, 
        // 服务端需不需要知道客户端的地址信息呢?
        // 显然需要知道, 因此accept的后两个参数
        // 就是发起请求方的地址信息
        struct sockaddr_in client; // 输出型参数
        bzero(&client, sizeof client);
        socklen_t client_len = sizeof client; // 输入输出型参数
        
        // 另外, 如果服务端没有收到连接请求
        // 那么服务端会在accept内部阻塞
        // 如果有连接请求, 那么我就处理
        // 连接成功, 返回一个套接字, 服务套接字
        // 连接失败, 返回 -1;
        
        int server_sock = accept(_listen_sock, reinterpret_cast<struct sockaddr*>(&client), &client_len);
        if(server_sock == -1)
        {
          LogMessage(FATAL, "%s,%d, Line:%d\n", strerror(errno), __LINE__);
          exit(4);
        }

        LogMessage(DEBUG, "accept success, return server_sock: %d\n", server_sock);

        // 走到这里, 说明连接成功
        // 就可以进行通信了
        
        // 因为我们线程池已经就绪
        // 那么此时我们就可以构造任务
        Xq::Task task(server_sock, client, communicate_data);
        // push到线程池中的任务标中
        _only_thread_pool->push_task(task);
        // 我们已经在 run_all_thread 内部调用了线程分离
        // 因此不用担心内存泄漏问题
      }
    }

    // 简单的echo服务器: 将客户端发送的数据返回给客户端
    static void communicate_data(int server_sock, const struct sockaddr_in& client, const std::string &name)
    {
      // 提取客户端的ip, 网络字节序 -> 主机序列
      uint16_t client_port = ntohs(client.sin_port);
      // 提取客户端的port, 网络字节序 -> 主机序列
      std::string client_ip = inet_ntoa(client.sin_addr);
      
      char buffer[SERVER_BUFFER] = {0};

      while(true)
      {
        buffer[0] = 0;
        // read  --- 读取客户端数据
        ssize_t read_size = read(server_sock, buffer, SERVER_BUFFER - 1);

        if(read_size > 0)
        {
          LogMessage(DEBUG, "%s read client data success, data size: %d\n", name.c_str(), read_size);
          buffer[read_size] = 0;
          printf("[%s][%d]: %s\n", client_ip.c_str(), client_port, buffer);
        }
        else if(read_size == 0)
        {
          // 代表客户端关闭连接了
          LogMessage(NORMAL, "client switch off connect, me too\n");
          break;
        }
        else
        {
          LogMessage(ERROR, "%d,%s\n", strerror(errno));
          exit(5);
        }

        // write --- 向客户端写数据
        ssize_t write_size = write(server_sock, buffer, strlen(buffer));
        if(write_size > 0)
        {
          LogMessage(DEBUG, "write_size: %d\n", write_size);
        }

      }
    }
    ~tcp_server() {}

  private:
    std::string _ip;  // IP
    uint16_t _port;   // 端口
    int _listen_sock; // 监听套接字
    Xq::thread_pool<Xq::Task>* _only_thread_pool;  // 线程池
  };
}

#endif

在网络通信中,如果服务器在客户端连接建立后一直保持这个连接,并且服务端的线程一直在处理客户端的请求和响应,这种连接就被称为长连接。

多进程或者多线程一定要有执行流的上限。 为什么呢? 对于一个服务器来说, 如果来一个客户端请求服务端就创建一个进程或者是线程, 那么如果一瞬间来了大量的客户端请求呢?如果此时服务端仍旧一一创建执行流,那么很有可能会导致服务器挂掉,甚至操作系统都有可能受到影响。

而线程池就有一个非常明显的好处。 哪怕你业务量在大, 我就这么多执行流, 不会出现创建大量执行流而导致服务器出现问题。

换言之, 线程池的执行流是有限个的, 因此,我们尽量不要保持这种长连接, 因为如果线程池中的所有线程都在处理客户端连接,那么再来新的客户端请求,只能被阻塞了。

所以,在一般情况下,最好不要保持长连接,而是在客户端请求到达时,服务端处理完请求后立即关闭连接。这样可以释放资源,并让线程池中的线程可以用于处理其他请求,提高系统的并发能力。

有了上面的理解,如果我们想实现一个简单的英译汉词典,如何实现呢? 实际上,我们就只需要更改一下线程的回调即可, 具体就是更改上面的 communicate data 函数即可。

2.7.3. version 4.1 版本的 Tcp.Server.hpp

简单的英译汉词典, 并且客户端连接处理完后,就断开连接,客户端下次在通信时,在进行connect。

#ifndef __TCP_SERVER_HPP_
#define __TCP_SERVER_HPP_

#include <iostream>
#include <string>
#include <cstring>

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

#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

#include <pthread.h>
#include <unordered_map>

#include "./thread_pool/Log.hpp"
#include "./thread_pool/ThreadPool.hpp"
#include "./thread_pool/Task.hpp"

#define SERVER_BUFFER 1024

namespace Xq
{
  const static int backlog = 20;

  class tcp_server
  {
  public:
    tcp_server(uint16_t port, const std::string& ip = "")
    :_ip(ip), _port(port), _listen_sock(-1)
    {
      // 获得线程池唯一实例
      _only_thread_pool = Xq::thread_pool<Xq::Task>::get_ptr_only_thread_pool(10);
    }

    void init_server()
    {
      // step 1: create socket
      // AF_INET, 表示IPV4互联网协议
      // SOCK_STREAM 表示这是面向字节流的
      // 此时就表示创建一个基于IPV4地址族、面向字节流的套接字
      _listen_sock = socket(AF_INET, SOCK_STREAM, 0);
      if(_listen_sock == -1)
      {
        LogMessage(FATAL, "%s,%d, Line:%d\n", strerror(errno), __LINE__);
        exit(1);
      }

      LogMessage(DEBUG, "socket success, return listen sock: %d\n", _listen_sock);

      // step 2: bind socket
      struct sockaddr_in server;
      server.sin_family = AF_INET;
      server.sin_port = htons(_port);
      // 服务端采用任意地址
      server.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

      if(-1 == bind(_listen_sock, reinterpret_cast<const struct sockaddr*>(&server), sizeof server))
      {
        LogMessage(FATAL, "%s,%d, Line:%d\n", strerror(errno), __LINE__);
        exit(2);
      }

      // 上面的过程和UDP几乎没有任何区别,
      // 无非UDP是面向数据报 --- SOCK_DGRAM
      // 而TCP是面向字节流的 --- SOCK_STREAM
      

      // step 3: set listen socket
      // TCP是面向连接的, 因此客户端和服务端首先要建立连接
      // 而对于服务端来说, 它并不知道客户端会在什么时候发起连接请求
      // 因此服务端是不是应该保持一种等待获取客户端连接请求的状态呢?
      // 答案是的, 因此listen这个系统调用就是将特定套接字设置为监听状态,
      // 获取客户端发起的连接请求。
      // backlog: 这个数字在这里暂时不解释, 后面解释
      // 一般情况下, 这个值不能太大, 也不能太小
      if(listen(_listen_sock, backlog) == -1)
      {
        LogMessage(FATAL, "%s,%d, Line:%d\n", strerror(errno), __LINE__);
        exit(3);
      }
      // server init done
      LogMessage(DEBUG, "server init success\n");
    }


    void start() 
    {
      // 我们采用线程池的方案
      // 让线程池去处理客户端连接
      // 线程池初始化后, 启动线程
      _only_thread_pool->run_all_thread();

      // 子进程退出, 自动被操作系统回收, 避免僵尸问题
      signal(SIGCHLD, SIG_IGN);
      // 服务器永不退出
      while(true)
      {
        // step 4: accept
        // accept用于接收客服端发送的连接请求
        // 那么客户端向服务端发送连接请求, 
        // 服务端需不需要知道客户端的地址信息呢?
        // 显然需要知道, 因此accept的后两个参数
        // 就是发起请求方的地址信息
        struct sockaddr_in client; // 输出型参数
        bzero(&client, sizeof client);
        socklen_t client_len = sizeof client; // 输入输出型参数
        
        // 另外, 如果服务端没有收到连接请求
        // 那么服务端会在accept内部阻塞
        // 如果有连接请求, 那么我就处理
        // 连接成功, 返回一个套接字, 服务套接字
        // 连接失败, 返回 -1;
        
        int server_sock = accept(_listen_sock, reinterpret_cast<struct sockaddr*>(&client), &client_len);
        if(server_sock == -1)
        {
          LogMessage(FATAL, "%s,%d, Line:%d\n", strerror(errno), __LINE__);
          exit(4);
        }

        LogMessage(DEBUG, "accept success, return server_sock: %d\n", server_sock);

        // 走到这里, 说明连接成功
        // 就可以进行通信了
        
        // 因为我们线程池已经就绪
        // 那么此时我们就可以构造任务
        Xq::Task task(server_sock, client, english_to_chinese);
        // push到线程池中的任务标中
        _only_thread_pool->push_task(task);
        // 我们已经在 run_all_thread 内部调用了线程分离
        // 因此不用担心内存泄漏问题
      }
    }

    static void english_to_chinese(int server_sock, const struct sockaddr_in& client, const std::string &name)
    {
      // 提取客户端的ip, 网络字节序 -> 主机序列
      uint16_t client_port = ntohs(client.sin_port);
      // 提取客户端的port, 网络字节序 -> 主机序列
      std::string client_ip = inet_ntoa(client.sin_addr);
      // 缓冲区,用于存放客户端传递过来的数据
      char buffer[SERVER_BUFFER] = {0};
      std::string ret_message_to_client = "";

      ssize_t read_size = read(server_sock, buffer, SERVER_BUFFER - 1);
      auto it = _dict.find(buffer);
      if(read_size > 0)
      {
        LogMessage(DEBUG, "%s read client data success, data size: %d\n", name.c_str(), read_size);
        buffer[read_size] = 0;
        if(it == _dict.end())
        {
          ret_message_to_client = "我不知道";
        }
        printf("[%s][%d]: %s\n", client_ip.c_str(), client_port, buffer);
      }
      else if(read_size == 0)
      {
        // 代表客户端关闭连接了
        LogMessage(NORMAL, "client switch off connect, me too\n");
        return ;
      }
      else 
      {
        // 读取错误
        LogMessage(ERROR, "%d,%s\n", strerror(errno));
        exit(5);
      }

      // write --- 向客户端写数据
      
      if(_dict.end() != it)
      {
        ret_message_to_client = it->second;
      }
      ssize_t write_size = write(server_sock, ret_message_to_client.c_str(), ret_message_to_client.size());
      if(write_size > 0)
      {
        LogMessage(DEBUG, "write_size: %d\n", write_size);
      }
    }

    ~tcp_server() {}

  private:
    std::string _ip;  // IP
    uint16_t _port;   // 端口
    int _listen_sock; // 监听套接字
    Xq::thread_pool<Xq::Task>* _only_thread_pool;  // 线程池
    static std::unordered_map<std::string, std::string> _dict;
  };
  std::unordered_map<std::string, std::string> tcp_server::_dict = \
    {{"victory","胜利"}, {"step", "步骤"}, {"process", "进程"}};
}
#endif

2.7.4.  与version 4.1版的服务端匹配的客户端

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

const int CLIENT_BUFFER = 1024;

void Usage(void)
{
  printf("please usa ./tcp_client server_ip  server_port\n");
  exit(1);
}

int main(int arg, char** argv)
{
  if(arg != 3)
  {
    Usage();
  }

  // 因为, 当客户端连接被处理完毕后
  // 服务端的执行流就不会再处理这个客户端的连接了
  // 因此如果还想继续向服务端发送数据
  // 需要客户端重新连接
  
  while(true)
  {
    // step 1: create socket
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock == -1)
    {
      LogMessage(FATAL, "%s,%d\n",strerror(errno));
      exit(2);
    }

    // 获取服务端port, 并将其转为网络字节序
    int server_port = htons(atoi(argv[2]));
    // 获取服务端IP, 并将其转为网络字节序
    uint32_t server_ip = inet_addr(argv[1]);

    // 填充服务端地址信息
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = server_ip;
    server.sin_port = server_port;
    socklen_t server_len = sizeof(server);


    if(connect(sock, reinterpret_cast<const struct sockaddr*>(&server), server_len) == -1 ) 
    {
      LogMessage(FATAL, "client connect failed\n");
      exit(3);
    }

    LogMessage(DEBUG, "client connect success\n");

    // 如果connect成功代表着该套接字(sock)
    // 与这个地址信息强关联的进程(服务端进程)建立了连接
    // 此时操作系统就会自动为客户端绑定
    // 接下来, 客户端就可以和服务端交互数据了
    
     std::cout << "请输入信息: " << std::endl;
     std::string message;
     getline(std::cin, message);

     if(message == "quit")
     {
       LogMessage(DEBUG, "client quit\n");
       break;
     }

     // 客户端向服务端发送数据 --- send
     ssize_t real_send_size = send(sock, message.c_str(), message.size(), 0);
     if(real_send_size > 0)
     {
       LogMessage(DEBUG, "send success\n");
     }
     else if(real_send_size == 0)
     {
       LogMessage(WARNING, "server disconnect\n");
       break;
     }
     else
     {
       LogMessage(ERROR, "send failed\n");
       break;
     }

     // 客户端收数据--- recv
     char buffer[CLIENT_BUFFER] = {0};
     ssize_t real_recv_size = recv(sock, buffer, CLIENT_BUFFER - 1, 0);
     if(real_recv_size > 0)
     {
       buffer[real_recv_size] = 0;
       printf("server: %s\n", buffer);
     }
     else
     {
       LogMessage(ERROR, "recv error\n");
       break;
     }
     close(sock);
  }

  return 0;
}

3. 补充问题

1、 为什么客户端不需要显示绑定呢?

在客户端,通常不需要显式地绑定套接字。这是因为,如果客户端显式的bind了, 那么是不是就要求客户端一定bind了一个固定的IP和port, 那么如果其他客户端占用了这个port呢? 那不就会导致port失败吗?因此,客户端不需要显式的bind,而是操作系统自动bind,当客户端调用connect时,操作系统会自动选择IP (通常是本地机器的地址) 和 port 用以绑定客户端创建的套接字。

2、 为什么服务端使用 INADDR_ANY 这样的特殊地址来绑定套接字呢?

使用任意IP,可以使服务器能够接受来自任意IP地址的客户端连接,只要端口号一定, 凡是给我这台主机的数据,我都可以收到,因此,服务端一般只需要指明端口,IP采用任意地址方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/449788.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【存储】ZYNQ+NVMe小型化全国产存储解决方案

文章目录 1、背景2、基础理论3、设计方案3.1、FPGA设计方案3.1.1、NVMe控制器实现3.1.2、NVMe控制器实现 3.2 驱动软件设计方案3.2.1 读写NVMe磁盘软件驱动3.2.2 NVMe磁盘驱动设计3.2.3 标准EXT4文件系统设计 3.3 上位机控制软件设计方案 4、测试结果4.1 硬件测试平台说明4.2 测…

【鸿蒙 HarmonyOS 4.0】Web组件

一、介绍 页面加载是Web组件的基本功能。根据页面加载数据来源可以分为三种常用场景&#xff0c;包括加载网络页面、加载本地页面、加载HTML格式的富文本数据。 二、加载网页 2.1、加载在线网页 Web组件的使用非常简单&#xff0c;只需要在Page目录下的ArkTS文件中创建一个…

Python 导入Excel三维坐标数据 生成三维曲面地形图(面) 1、线条折线曲面

环境和包: 环境 python:python-3.12.0-amd64包: matplotlib 3.8.2 pandas 2.1.4 openpyxl 3.1.2 代码: import pandas as pd import matplotlib.pyplot as plt import numpy as np from mpl_toolkits.mplot3d import Axes3D from matplotlib.colors import ListedColo…

基于Python3的数据结构与算法 - 15 栈和队列的应用(迷宫问题)

题目 给一个二维列表&#xff0c;表示迷宫&#xff08;0表示通道&#xff0c;1表示围墙&#xff09;。给出算法&#xff0c;求一条走出迷宫的路径。 方法一 &#xff1a;使用栈 -- 深度优先搜索 方法&#xff1a;回溯法 思路&#xff1a;从一个节点开始&#xff0c;任意找下…

OpenCV开发笔记(七十七):相机标定(二):通过棋盘标定计算相机内参矩阵矫正畸变摄像头图像

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/136616551 各位读者&#xff0c;知识无穷而人力有穷&#xff0c;要么改需求&#xff0c;要么找专业人士&#xff0c;要么自己研究 红胖子(红模仿)的博…

基于Java+SpringBoot+vue+element实现汽车订票管理平台详细设计和实现

基于JavaSpringBootvueelement实现汽车订票管理平台详细设计和实现 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 …

开源好用的所见即所得(WYSIWYG)编辑器:Editor.js

文章目录 特点基于区块干净的数据 界面与交互插件标题和文本图片列表Todo表格 使用安装创建编辑器实例配置工具本地化自定义样式 今天介绍一个开源好用的Web所见即所得(WYSIWYG)编辑器&#xff1a; Editor.js Editor.js 是一个基于 Web 的所见即所得富文本编辑器&#xff0c;它…

【毕设级项目】基于AI技术的多功能消防机器人(完整工程资料源码)

基于AI技术的多功能消防机器人演示效果 竞赛-基于AI技术的多功能消防机器人视频演示 前言 随着“自动化、智能化”成为数字时代发展的关键词&#xff0c;机器人逐步成为社会经济发展的重要主体之一&#xff0c;“机器换人”成为发展的全新趋势和时代潮流。在可预见的将来&#…

鸿蒙App基础

基础说明 .1、应用模型 .1.1、构成要素 应用组件 应用组件是应用的基本组成单位&#xff0c;是应用的运行入口。用户启动、使用和退出应用过程中&#xff0c;应用组件会在不同的状态间切换&#xff0c;这些状态称为应用组件的生命周期。应用组件提供生命周期的回调函数&…

如何提高API接口的性能和设计安全可靠的API

如何提高API接口的性能 下图显示了提高 API 性能的 5 种常见技巧。 分页 这是在结果集较大时常用的优化方法。结果会以流式方式传回客户端&#xff0c;以提高服务响应速度。 异步日志 同步日志每次调用都要处理磁盘&#xff0c;会降低系统速度。异步日志会先将日志发送到无…

力扣226.翻转二叉树(二叉树的先序遍历)

文章目录 题目描述思路复杂度Code 题目描述 思路 利用二叉树的先序遍历&#xff0c;每次递归遍历时将当前节点的左右子节点交换即可 复杂度 时间复杂度: O ( n ) O(n) O(n)&#xff1b;其中 n n n为树节点的个数 空间复杂度: O ( h e i g h ) O(heigh) O(heigh)&#xff1b;其…

任务执行中拖延的认知神经机制

任务执行中拖延的认知神经机制 引言 虽然动机的产生往往是人们行动的前提(Ajzen, 2011;林琳&白新文,2014)&#xff0c;但动机的产生却并不必然地导致随后的行为(Rhodes&deBruijn,2013;Sniehotta, Scho1z, & Schwarzer, 2005)。动机向行为的转换依赖于一系列自我控…

北京市行政村边界shp数据/北京市乡镇边界/北京市土地利用分类数据

北京是一座有着三千多年历史的古都&#xff0c;在不同的朝代有着不同的称谓&#xff0c;大致算起来有二十多个别称。北京地势西北高、东南低。西部、北部和东北部三面环山&#xff0c;东南部是一片缓缓向渤海倾斜的平原。境内流经的主要河流有&#xff1a;永定河、潮白河、北运…

Ollama 只安装 Ollama,本地快速部署谷歌开源大模型Gemma(基于Ollama)

参考&#xff1a;本地快速部署谷歌开源大模型Gemma(基于Ollama) - 知乎 确保系统更新: Bash sudo apt update && sudo apt upgrade 需要先下载Ollama&#xff0c;版本要求0.1.26及以上 运行curl -fsSL https://ollama.com/install.sh | sh 监听 Ollama API 接…

常见的限流算法- python版本

shigen坚持更新文章的博客写手&#xff0c;擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长&#xff0c;分享认知&#xff0c;留住感动。 个人IP&#xff1a;shigen 在系统的稳定性设计中&#xff0c;需要考虑到的就是限流&#xff0c;避免高并发…

【VUe】简略学习 vue

Vue 是一套用于构建用户界面的渐进式框架。要想使用这个框架&#xff0c;就需要先在页面中引用&#xff1a; 如何使用&#xff1a; 来到控制台&#xff1a; 数据绑定 若要在标签里替换&#xff0c;就需要使用 v-bind 指令了&#xff1a; 在标签里&#xff08;尖括号里&#xf…

数据库insert详细用法

数据库版本&#xff1a;KingbaseES V008R006C008B0014 简介 INSERT 语句用于将数据插入表中&#xff0c;向指定表格添加1行或多行数据&#xff0c;本篇文章主要以kingbase介绍insert的一些技巧。 文章目录如下 1. 基本语法 2. 实用技巧 2.1. 插入其他表数据 2.2. 快速插入万…

【力扣hot100】刷题笔记Day25

前言 这几天搞工作处理数据真是类似我也&#xff0c;还被老板打电话push压力有点大的&#xff0c;还好搞的差不多了&#xff0c;明天再汇报&#xff0c;赶紧偷闲再刷几道题&#xff08;可恶&#xff0c;被打破连更记录了&#xff09;这几天刷的是动态规划&#xff0c;由于很成…

HTML5七天学会基础动画网页10(2)

制作立方体 学完前面的基础内容&#xff0c;制作立方体是个不错的练习方法&#xff0c;先看成品 再分析一下&#xff0c;六个面让每个面旋转平移就可以实现一个立方体&#xff0c;来看代码: <title> 制作立方体</title> <style> *{ margin: 0; padding: 0; …

鸿蒙开发:从入门到精通的全方位学习指南

随着华为鸿蒙HarmonyOS生态系统的迅速扩展&#xff0c;越来越多的开发者渴望深入了解并掌握这一前沿技术。本文旨在为鸿蒙开发新手提供一份详尽且实用的学习教程&#xff0c;助您从零开始&#xff0c;逐步迈向鸿蒙开发的巅峰。 一、鸿蒙开发环境搭建 DevEco Studio安装&#x…