套接字编程 --- 三

news2024/12/23 13:20:31

目录

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采用任意地址方案。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[AIGC] Kafka解析:分区、消费者组与消费者的关系

Apache Kafka是一个分布式事件流平台&#xff0c;它是处理实时数据的强大工具。而理解Kafka的关键概念&#xff1a;分区&#xff08;Partition&#xff09;、消费者组&#xff08;Consumer Group&#xff09;和消费者&#xff08;Consumer&#xff09;的关系对于正确地使用Kafk…

C语言--- 指针运算笔试题详解

目录 题目1&#xff1a; 题目2&#xff1a; 题目3&#xff1a; 题目4&#xff1a; 题目5&#xff1a; 题目6&#xff1a; 题目7&#xff1a; 题目1&#xff1a; #include <stdio.h> int main() {int a[5] { 1, 2, 3, 4, 5 };int *ptr (int *)(&a 1);print…

C#与WPF通用类库

个人集成封装&#xff0c;仓库已公开 NetHelper 集成了一些常用的方法&#xff1b; 如通用的缓存静态操作类、常用的Wpf的ValueConverters、内置的委托类型、通用的反射加载dll操作类、Wpf的ViewModel、Command、Navigation、Messenger、部分常用UserControls(可绑定的Passwo…

蓝桥杯-ISBN号码

此题然让本人纠结了很久&#xff0c;真的好多坑。。。。果然还是太菜了。 完整代码以及思路解析(在注释中) #include <iostream> using namespace std; int main() {string num;cin>>num; int count0;int w1;for(int i0;i<10;i){if((i!1)&&(i!5)) //坑…

Node.js作用

Node.js可以开发应用 开发服务器应用 开发工具类应用 开发桌面端应用

Discord OAuth2授权以及机器人监听群事件

下面文章讲解获取OAuth2授权整个流程&#xff0c;创建机器人&#xff0c;使用机器人监听工会&#xff08;工会就是创建的服务器&#xff09;成员变化等等&#xff0c;对接国外的都是需要VPN的哦&#xff0c;对接的时候记得提前准备。 创建应用 点击 此页面添加应用,&#xff…

EI级 | Matlab实现GCN基于图卷积神经网络的数据多特征分类预测

EI级 | Matlab实现GCN基于图卷积神经网络的数据多特征分类预测 目录 EI级 | Matlab实现GCN基于图卷积神经网络的数据多特征分类预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.GCN基于图卷积神经网络的数据分类预测 Matlab2023 2.多输入单输出的分类预测&#xf…

汽车电子:均胜电子、德赛西威“跑出”第二增长曲线

问界新M7上市以来销量势如破竹&#xff0c;华为汽车及相关均胜电子、联创电子、光峰科技等供应链企业也因此受益。 网络公开数据&#xff0c;在2024年1月1日-2月18日&#xff0c;中国市场新势力品牌销量排行榜中&#xff0c;问界就以4.25万辆的销量成绩霸榜第一&#xff0c;作…

幻兽帕鲁Palworld服务器搭建费用,仅需26元!

2024阿里云幻兽帕鲁专用服务器价格表&#xff1a;4核16G幻兽帕鲁专用服务器26元一个月、149元半年&#xff0c;默认10M公网带宽&#xff0c;8核32G幻兽帕鲁服务器10M带宽价格90元1个月、271元3个月。阿里云提供的Palworld服务器是ECS经济型e实例&#xff0c;CPU采用Intel Xeon …

政务网站安全合规之道,云监测提供优质监测解决方案

近年来&#xff0c;国家对于网站安全风险的问题重视程度不断提升&#xff0c;持续加强对网站安全的监管力度。特别是政务网站&#xff0c;承载着越来越重要的核心应用和数据&#xff0c;与普通网站相比更容易遭到来自互联网的攻击。 攻击者为了破坏政务形象、干扰政务工作秩序或…

RC4算法:流密码算法的经典之作

title: RC4算法&#xff1a;流密码算法的经典之作 date: 2024/3/11 18:16:16 updated: 2024/3/11 18:16:16 tags: RC4起源演变算法优劣分析RC4 vs AES安全性RC4 vs DES性能比较应用场景介绍工作原理详解代码实例演示 一、RC4算法的起源与演变 RC4算法是由著名密码学家Ron Riv…

Oracle数据库迁移至达梦8数据库(windows图文讲解)

写作不易&#xff0c;如果对各位有用请给个赞支持一下~ 1.第一步安装达梦8数据库&#xff1a; 下载安装看我这篇博客 https://blog.csdn.net/li836779537/article/details/136641411?spm1001.2014.3001.5502 2.废话不多说直接开干&#xff0c;安装完成后打开迁移工具 名字…

指令微调(Instructional Fine-tuning)

定义 指令微调&#xff08;Instructional Fine-tuning&#xff09;是一种自然语言处理&#xff08;NLP&#xff09;技术&#xff0c;特别是在大型预训练语言模型&#xff08;如 GPT、BERT 等&#xff09;的应用中。在指令微调中&#xff0c;模型被进一步训练以更好地理解和遵循…

Linux--ELK 日志分析系统

ELK &#xff08;Elasticsearch、Logstash、Kibana&#xff09;日志分析系统的好处是可以集中查看所有服务器日志&#xff0c;减轻了工作量&#xff0c;从安全性的角度来看&#xff0c;这种集中日志管理可以有效查询以及跟踪服务器被攻击的行为。 > Elasticsearch 是个开源分…

容器安全是什么?

容器安全定义 容器安全是指保护容器的完整性。这包括从其保管的应用到其所依赖的基础架构等全部内容。容器安全需要完整且持续。通常而言&#xff0c;企业拥有持续的容器安全涵盖两方面&#xff1a; 保护容器流水线和应用保护容器部署环境和基础架构 如何将安全内置于容器流…

[BUG] docker运行Java程序时配置代理-Dhttp.proxyHost后启动报错

[BUG] docker运行Java程序时配置代理-Dhttp.proxyHost后启动报错 bug现象描述 版本&#xff1a;2.0.4&#xff08;客户端和服务端都是&#xff09; 环境&#xff1a;私有云环境&#xff0c;只有少量跳板机器可以访问公网&#xff0c;其他机器均通过配置代理方式访问公网 bug现…

CSS中grid网格布局详解

文章目录 一、是什么二、属性display 属性grid-template-columns 属性&#xff0c;grid-template-rows 属性grid-row-gap 属性&#xff0c; grid-column-gap 属性&#xff0c; grid-gap 属性grid-template-areas 属性grid-auto-flow 属性justify-items 属性&#xff0c; align-…

数据结构 之 链表LinkedList

目录 1. ArrayList的缺陷&#xff1a; 2. 链表&#xff1a; 2.1 链表的概念及结构&#xff1a; 3. 链表的使用和模拟实现&#xff1a; 3.1 构造方法&#xff1a; 3.2 模拟实现&#xff1a; 4. 源码分享&#xff1a; 在我学习顺序表之后&#xff0c;我就立马开始了链表的学…

idea 导入项目

idea 导入项目并运行 导入设置设置 jdk查看maven 设置 导入 在项目首页 或者 file 选择 open, 然后选择项目根路径 设置 设置 jdk 查看maven 设置

Vue 中的 key:列表渲染的秘诀

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…