当前位置:   article > 正文

TCP网络程序

TCP网络程序

上一章我们基于UDP实现了几个网络程序,这一章我们开始使用TCP。

先简单复习一下TCP和UDP的特点:

  • TCP特点
  1. 传输层协议
  2. 有连接
  3. 可靠传输
  4. 面向字节流
  • UDP特点
  1. 传输层协议
  2. 无连接
  3. 不可靠传输
  4. 面向数据报

可以看到TCP是有链接的,而UDP是无连接的,有无连接体现在接口上面(listen,connect),话不多说,直接开始。

1.服务器的实现

 1.1socket(创建套接字)

  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. int socket(int domain, int type, int protocol);

参数说明:

  • domain:域,标识了这个套接字的通信类型(网络通信/本地通信),这个就是sockaddr结构体的前16个bit位。如果是本地通信就是AF_UNIX,如果是网络通信就是AF_INET。 
  • type:套接字提供的服务类型,常见的就是SOCK_STREAM和SOCK_DGRAM,如果我们是基于TCP协议的通信,就使用SOCK_STREAM,表示的是流式服务。如果我们是基于UDP协议通信的,就使用SOCK_DGRAM,表示的是数据包服务。
  • protocol:创建套接字的类型(TCP/UDP),但是这个参数可以由前两个参数决定。所以通常设置为0

返回值:

成功会返回一个文件描述符,失败返回-1,错误码被设置

在系统的文件操作中,也会返回文件描述符,与socket返回的文件描述符不同的是,普通文件的文件缓冲区对应的是磁盘,而socket返回的文件描述符的文件缓冲区对应的是网卡。用户将数据写到缓冲区,由操作系统自动将缓冲区中的数据刷新到网卡中,网卡会负责将这个数据发送到对端主机上。

  1. class TcpServer
  2. {
  3. public:
  4. TcpServer(uint16_t &port, std::string ip = "")
  5. {
  6. // 创建套接字
  7. _listensock = socket(AF_INET, SOCK_STREAM, 0);
  8. if (_listensock < 0)
  9. {
  10. std::cout << "create socket error" << std::endl;
  11. exit(1);
  12. }
  13. std::cout << "create socket success, socket: " << _listensock << std::endl;
  14. }
  15. ~TcpServer()
  16. {
  17. close(_listensock);
  18. }
  19. private:
  20. int _listensock;
  21. };

调用socket函数,就能创建一个套接字了,第一个参数我们填AF_INET,表示的是我们是网络通信。第二个参数我们填SOCK_STREAM,表示我们是TCP服务(字节流)。由于一个进程启动时,默认会打开标准输入,标准输出,标准错误这三个文件描述符,而文件描述符的创建规则就是从0开始,向上找到第一个没有使用的,所以我们可以猜测以下_sock的值为3.

这一步和UDP非常类似,但是在socket的时候,第二个参数要更改成SOCK_STREAM表示我们要的是TCP服务。


1.2bind(绑定IP和Port)

我们创建完套接字以后,也只是打开了一个文件。还没有将这个文件和网络关联起来,所以我们需要使用bind函数,将IP+port和文件绑定

  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数介绍:

  • sockfd:socket函数的返回值。
  • addr:通用结构体,包括协议家族,IP地址,端口号port
  • addrlen:addr的长度

返回值介绍:

成功返回0,错误返回-1

sockaddr_in结构体

  1. struct sockaddr_in
  2. {
  3. short int sin_family;
  4. in_port_t sin_port;
  5. struct in_addr sin_addr;
  6. unsigned char sin_zero[8];
  7. };
  • sin_family:协议家族,表示通信类型(AF_INET网络通信,PF_INET本地通信)
  • sin_port:端口号(网络字节序)
  • sin_addr:IP地址。
  • sin_zero:填充字段,让sizeof(sockaddr_in) = 16

既然第二步需要添加IP和端口号,所以我们要对代码修改一下,给构造函数添加port和str(IP的点分十进制表示形式)

  1. class TcpServer
  2. {
  3. public:
  4. TcpServer(uint16_t &port, std::string ip = "")
  5. {
  6. // 创建套接字
  7. _listensock = socket(AF_INET, SOCK_STREAM, 0);
  8. if (_listensock < 0)
  9. {
  10. std::cout << "create socket error" << std::endl;
  11. exit(1);
  12. }
  13. std::cout << "create socket success, socket: " << _listensock << std::endl;
  14. // 绑定IP和端口
  15. struct sockaddr_in addr;
  16. addr.sin_family = AF_INET;
  17. addr.sin_port = htons(port);
  18. addr.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());
  19. if (bind(_listensock, (sockaddr *)&addr, sizeof(addr)) < 0)
  20. {
  21. std::cout << "bind error" << std::endl;
  22. exit(2);
  23. }
  24. std::cout << "bind success" << std::endl;
  25. }
  26. ~TcpServer()
  27. {
  28. close(_listensock);
  29. }
  30. private:
  31. int _listensock;
  32. };

需要注意的是在填写sockaddr_in的参数的时候,

  • sin_family需要和socket套接字创建时的domain相同。
  • 由于端口号和IP将来是要发送到网络的,而网络数据流统一采用大端的形式,所以为了代码的可移植性,我们不管自己是大端还是小端,统一调用函数hton转化成大端。
  • 对于IP来说,如果直接发送的是点分十进制形式如(192.168.12.80)的形式,则需要占用过多的字节数,对网络传输无疑是一种很大的消耗,所以我们采用一个32位的整数来表示IP,在网络传输中,要将点分十进制的IP转化成整数,再进行传输。

前两步和UDP基本上一样,不过UDP做完这两步之后就可以开始接受用户的信息了,因为UDP是无连接的。但是TCP还需要一个监听的动作(监听客户端发来的连接请求)。

我们需要把socket套接字设置为监听状态,获取客户端发来的连接请求。例如:假设有这样一个手机,默认的工作模式就是拦截所有的电话,用户无法知道,但是用户可以更改工作模式为监听模式,不再拦截电话,只要有人给我打电话我就知道。这种监听模式就是类似于listen函数的功能。

1.3listen(设置监听状态)

  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. int listen(int sockfd, int backlog);

参数说明:

  • sockfd:要设置为监听状态的文件描述符
  • backlog:等待队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列。它并不限制可以建立的总连接数,而是限制了在任何给定时间等待接受的连接数。一旦一个连接被接受,它就从队列中移除,并为新的连接请求腾出空间。一般不要设置太大,设置为5或10即可。

重要的是要注意,listen 函数本身并不接受任何连接。它只是将套接字设置为监听模式,并设置等待队列的大小。实际的连接接受是通过 accept 函数来完成的。


1.4accept(接受新连接)

前面的工作都做完了,我们就可以使用accept来接受新连接了。

  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数介绍:

  • sockfd:表示从该监听套接字中获取新连接
  • addr:通用结构体,包括协议家族,IP地址,端口号port
  • addrlen:输出型参数,调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度。

返回值:

成功返回一个文件描述符,失败返回-1

accept返回的文件描述符

先举个例子:当我们去吃饭时,门口会有个服务员张三正在揽客,当他找到客人就把客人交给了新的服务员李四/王五/赵六,负责后续为我们点菜,上菜等服务。在这个例子中,张三的职责就是将客人招到店内就可以,店内的服务他不参与。而李四/王五/赵六才是为我们真正提供服务的员工。

而我们之前所说的使用socket返回的文件描述符就是张三,他的任务就是接收到新连接就可以了。真正提供服务的其实是accept返回的文件描述符

最后别忘了关闭文件描述符,如果不关会导致可用的文件描述符越来越少

  1. void Start()
  2. {
  3. while (true)
  4. {
  5. struct sockaddr_in addr;
  6. socklen_t addrlen = sizeof(addr);
  7. memset(&addr, 0, sizeof(addr));
  8. int serverSocket = accept(_listensock, (sockaddr *)&addr, &addrlen);
  9. //业务逻辑
  10. close(serverSocket);
  11. }
  12. }

1.5recvfrom(获取消息)

  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
  4. struct sockaddr *src_addr, socklen_t *addrlen);

参数介绍:

  • sockfd:从哪个套接字中读取
  • buf:读到的数据存放的缓冲区
  • len:读len个字节
  • flags:读取方式,0表示阻塞读取
  • src_addr:发送端的信息
  • addrlen:输出型参数,表示src_addr的长度(必须初始化为sizeof(src_addr))

返回值说明:

  • 返回值>0:读取成功
  • 返回值=0:对端关闭连接了
  • 返回值<0:读取失败

由于recvfrom的参数较多,使用比较麻烦,所以我们可以对recvfrom简单封装成一个函数。

  1. int Recvfrom(int sockfd, std::string& buffer)
  2. {
  3. char temp[1024];
  4. sockaddr_in addr;
  5. socklen_t addrlen = sizeof(addr);
  6. int n = recvfrom(sockfd, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
  7. if (n > 0)
  8. {
  9. temp[n] = 0;
  10. buffer = temp;
  11. }
  12. else if (n == 0)
  13. {
  14. std::cout << "you close the connect, me too" << std::endl;
  15. }
  16. else
  17. {
  18. std::cout << "recvfrom error" << std::endl;
  19. }
  20. return n;
  21. }

你只需要告诉recvfrom从哪个文件描述符当中读,并且给他一个buffer用于保存读取的消息。


1.6sendto(发送消息)

  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
  4. const struct sockaddr *dest_addr, socklen_t addrlen);

参数介绍:

  • sockfd:从哪个套接字中读取
  • buf:将缓冲区中的数据发给对端
  • len:要发送多少个字节
  • flags:写入方式,0表示阻塞写入
  • src_addr:对端主机的信息(包括协议家族,IP,port)
  • addrlen:表示src_addr的长度

参数和recvfrom类似。

  1. while (true)
  2. {
  3. std::string buffer;
  4. std::cout << "Please enter# " << std::endl;
  5. getline(std::cin, buffer);
  6. sendto(sockfd, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, addrlen);
  7. }

当我们接受到了客户端发来的消息后,就可以将这个消息经过处理后,还给客户端。


1.7服务端源码

到这里一个简单的服务器就可以正常启动了。

  1. #include <iostream>
  2. #include <cstring>
  3. #include <unistd.h>
  4. #include <functional>
  5. #include <unordered_map>
  6. #include <signal.h>
  7. #include <sys/types.h>
  8. #include <sys/socket.h>
  9. #include <netinet/in.h>
  10. #include <arpa/inet.h>
  11. #include <sys/wait.h>
  12. #include <pthread.h>
  13. class TcpServer
  14. {
  15. const int backlog = 10;
  16. public:
  17. TcpServer(uint16_t &port, std::string ip = "")
  18. {
  19. // 创建套接字
  20. _listensock = socket(AF_INET, SOCK_STREAM, 0);
  21. if (_listensock < 0)
  22. {
  23. std::cout << "create socket error" << std::endl;
  24. exit(1);
  25. }
  26. std::cout << "create socket success, socket: " << _listensock << std::endl;
  27. // 绑定IP和端口
  28. struct sockaddr_in addr;
  29. addr.sin_family = AF_INET;
  30. addr.sin_port = htons(port);
  31. addr.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());
  32. if (bind(_listensock, (sockaddr *)&addr, sizeof(addr)) < 0)
  33. {
  34. std::cout << "bind error" << std::endl;
  35. exit(2);
  36. }
  37. std::cout << "bind success" << std::endl;
  38. // 监听listen
  39. if (listen(_listensock, backlog) < 0)
  40. {
  41. std::cout << "listen error" << std::endl;
  42. exit(3);
  43. }
  44. std::cout << "listen success" << std::endl;
  45. }
  46. ~TcpServer()
  47. {
  48. close(_listensock);
  49. }
  50. void Start()
  51. {
  52. while (true)
  53. {
  54. struct sockaddr_in addr;
  55. socklen_t addrlen = sizeof(addr);
  56. memset(&addr, 0, sizeof(addr));
  57. int serverSocket = accept(_listensock, (sockaddr *)&addr, &addrlen);
  58. while (true)
  59. {
  60. std::string buffer;
  61. int n = Recvfrom(serverSocket, buffer);
  62. if (n > 0)
  63. std::cout << buffer << std::endl;
  64. else
  65. break;
  66. sendto(serverSocket, buffer.c_str(), buffer.size(), 0, (sockaddr*)&addr, sizeof(addr));
  67. }
  68. close(serverSocket);
  69. }
  70. }
  71. int Recvfrom(int sockfd, std::string& buffer)
  72. {
  73. char temp[1024];
  74. sockaddr_in addr;
  75. socklen_t addrlen = sizeof(addr);
  76. int n = recvfrom(sockfd, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
  77. if (n > 0)
  78. {
  79. temp[n] = 0;
  80. buffer = temp;
  81. }
  82. else if (n == 0)
  83. {
  84. std::cout << "you close the connect, me too" << std::endl;
  85. }
  86. else
  87. {
  88. std::cout << "recvfrom error" << std::endl;
  89. }
  90. return n;
  91. }
  92. private:
  93. int _listensock;
  94. };

2.客户端实现

2.1socket(创建套接字)

和服务端一样,我们也要创建套接字

  1. void Usage()
  2. {
  3. std::cout << "Please enter: ./Client ip port" << std::endl;
  4. }
  5. int main(int argc, char *argv[])
  6. {
  7. if (argc != 3)
  8. {
  9. Usage();
  10. return 4;
  11. }
  12. std::string ip = argv[1];
  13. uint16_t port = atoi(argv[2]);
  14. // 创建套接字
  15. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  16. if (sockfd < 0)
  17. {
  18. std::cout << "create socket error" << std::endl;
  19. exit(1);
  20. }
  21. std::cout << "create socket success, socket: " << sockfd << std::endl;
  22. return 0;
  23. }

2.2connect(发起连接)

客户端既不需要绑定bind,也不需要监听listen,直接向服务端发起connect即可。

  • 服务端需要绑定一个固定IP和端口,防止用户找不到,但是客户端如果绑定固定端口,可能会导致多个不同应用的客户端绑定的是同一个端口的,系统会在connect之后为客户端分配一个未使用的端口号。
  • 客户端也不需要listen,listen函数是用于接受来自其他方的连接的,而客户端都是主动发起连接的一方,例如:都是你主动打开抖音(向服务端发起连接)。
  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数介绍:

  • sockfd:用于标识即将发起连接请求的客户端套接字
  • addr:用于标识即将发起连接请求的客户端套接字
  • addrlen:addr的大小

返回值说明:

成功返回0,失败返回-1

  1. sockaddr_in addr;
  2. socklen_t addrlen = sizeof(addr);
  3. addr.sin_family = AF_INET;
  4. addr.sin_port = htons(port);
  5. addr.sin_addr.s_addr = inet_addr(ip.c_str());
  6. if (connect(sockfd, (sockaddr *)&addr, addrlen) < 0)
  7. {
  8. std::cout << "connect error" << std::endl;
  9. exit(2);
  10. }
  11. std::cout << "connect success" << std::endl;

2.3sendto(发送消息)

发送给服务端你要处理的消息

  1. while (true)
  2. {
  3. std::string buffer;
  4. std::cout << "Please enter# " << std::endl;
  5. getline(std::cin, buffer);
  6. sendto(sockfd, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, addrlen);
  7. }

因为前面使用connect已经填写了addr。所以我们可以直接使用。


2.4recvfrom(接受消息)

接受客户端已经处理好的消息

  1. //接受服务端的消息
  2. char temp[128];
  3. sockaddr_in raddr;
  4. socklen_t raddrlen = sizeof(addr);
  5. int n = recvfrom(sockfd, temp, sizeof(temp) - 1, 0, (sockaddr *)&raddr, &raddrlen);
  6. if (n <= 0)
  7. {
  8. std::cout << "recvfrom error" << std::endl;
  9. exit(3);
  10. }
  11. temp[n] = 0;
  12. std::cout << "echo# " << temp << std::endl;

2.5客户端源码

  1. #include <iostream>
  2. #include <sys/types.h>
  3. #include <sys/socket.h>
  4. #include <netinet/in.h>
  5. #include <arpa/inet.h>
  6. #include "LogMessage.hpp"
  7. void Usage()
  8. {
  9. std::cout << "Please enter: ./Client ip port" << std::endl;
  10. }
  11. int main(int argc, char *argv[])
  12. {
  13. if (argc != 3)
  14. {
  15. Usage();
  16. return 4;
  17. }
  18. std::string ip = argv[1];
  19. uint16_t port = atoi(argv[2]);
  20. // 创建套接字
  21. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  22. if (sockfd < 0)
  23. {
  24. std::cout << "create socket error" << std::endl;
  25. exit(1);
  26. }
  27. std::cout << "create socket success, socket: " << sockfd << std::endl;
  28. sockaddr_in addr;
  29. socklen_t addrlen = sizeof(addr);
  30. addr.sin_family = AF_INET;
  31. addr.sin_port = htons(port);
  32. addr.sin_addr.s_addr = inet_addr(ip.c_str());
  33. if (connect(sockfd, (sockaddr *)&addr, addrlen) < 0)
  34. {
  35. std::cout << "connect error" << std::endl;
  36. exit(2);
  37. }
  38. std::cout << "connect success" << std::endl;
  39. while (true)
  40. {
  41. std::string buffer;
  42. std::cout << "Please enter# " << std::endl;
  43. getline(std::cin, buffer);
  44. sendto(sockfd, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, addrlen);
  45. // 接受服务端的消息
  46. char temp[128];
  47. sockaddr_in raddr;
  48. socklen_t raddrlen = sizeof(addr);
  49. int n = recvfrom(sockfd, temp, sizeof(temp) - 1, 0, (sockaddr *)&raddr, &raddrlen);
  50. if (n <= 0)
  51. {
  52. std::cout << "recvfrom error" << std::endl;
  53. exit(3);
  54. }
  55. temp[n] = 0;
  56. std::cout << "echo# " << temp << std::endl;
  57. }
  58. return 0;
  59. }

3.运行程序

简单测试一下:

可以运行,这样一个简单TCP服务就写好了。那这段代码有问题吗?有,而且很大。

如果现在有两个用户想要访问这个服务端,上面那个用户可以正常运行,但是下面那个不行,消息并不会回显。左边的服务端也没有接受到客户端发送的消息。为什么呢?

  1. void Start()
  2. {
  3. while (true)
  4. {
  5. struct sockaddr_in addr;
  6. socklen_t addrlen = sizeof(addr);
  7. memset(&addr, 0, sizeof(addr));
  8. int serverSocket = accept(_listensock, (sockaddr *)&addr, &addrlen);
  9. while (true)
  10. {
  11. std::string buffer;
  12. int n = Recvfrom(serverSocket, buffer);
  13. if (n > 0)
  14. std::cout << buffer << std::endl;
  15. else
  16. break;
  17. sendto(serverSocket, buffer.c_str(), buffer.size(), 0, (sockaddr*)&addr, sizeof(addr));
  18. }
  19. close(serverSocket);
  20. }
  21. }

注意看服务端的代码,他是单进程的,并且在接收到用户发起的新连接之后,会进入下面的死循环,从此往后,他只能接受到来自第一个用户发送的消息了,只有在第一个用户断开连接之后,才会接受来自其他用户的连接。

客户为什么会显示连接成功

按照刚才的思路,服务端的执行流一直在执行下面接受第一个用户的消息,没有再次调用accept函数,但是为什么客户端会显示connect成功呢?

客户端connect成功,说明请求连接是成功的,但是服务端暂时没有调用accept获取该连接,底层会帮我们维护一个连接队列(listen的第二个参数)。将没有accept的新连接放到这个队列当中。虽然服务端还没有获取该连接,但是connect不关心服务端有没有获取,只关心有没有将新连接成功交给服务端,所以客户端显示连接成功。

将用户1关闭之后,用户而就可以正常的进行服务了

如何解决这个问题?

我们上面的问题基本上都是单执行流导致的,所以我们需要将我们的服务端改成多执行流即可。

4.多执行流

主要分为多进程版本和多线程版本。我们先将前面的服务(读取用户信息,经过处理再发给用户)封装一下

  1. void Start()
  2. {
  3. while (true)
  4. {
  5. //获取新连接
  6. struct sockaddr_in addr;
  7. socklen_t addrlen = sizeof(addr);
  8. memset(&addr, 0, sizeof(addr));
  9. int serverSocket = accept(_listensock, (sockaddr *)&addr, &addrlen);
  10. //执行任务
  11. Server(serverSocket, addr);
  12. close(serverSocket);
  13. }
  14. }
  15. void Server(int serverSocket, sockaddr_in addr)
  16. {
  17. while (true)
  18. {
  19. std::string buffer;
  20. int n = Recvfrom(serverSocket, buffer);
  21. if (n > 0)
  22. std::cout << buffer << std::endl;
  23. else
  24. break;
  25. sendto(serverSocket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
  26. }
  27. }

前面我们就是以上面这种单进程的方式进行的。

4.1多进程

让父进程获取新连接,并且创建子进程执行服务。

子进程会继承父进程的文件描述符表,当子进程创建的孙子进程也会继承父进程的文件描述符表。

对于父进程来说,创建完子进程之后,就可以关闭accpet返回的文件描述符了(后续服务让子进程进行),自己继续通过socket的返回值获取新连接,而对于子进程来说,socket返回的文件描述符也没有意义,所以可以关闭。

注意:对于父进程来说,一定要关闭accept返回的文件描述符,如果只分配不关闭的话,会导致文件描述符越来越多,可用的文件描述符越来越少,这种问题叫做文件描述符泄露。但是对于子进程来说,关闭文件描述符socket文件描述符不是必须的(但是最好也要关闭)

等待子进程问题

我们知道,在父进程创建子进程之后,需要等待子进程,否则子进程会变成僵尸进程,造成给内存泄露的问题,所以父进程需要调用wait或者waitpid等待子进程。

  • 但是如果采用了阻塞式等待的话,那就和单进程没什么区别了
  • 如果采用非阻塞式等待,虽然也可以,但是父进程需要不断消耗资源去检测有无子进程退出

无论哪种方法都不太合适,所以我们可以想出一种无需等待子进程的方法

4.1.2孙子进程

  1. int fd = fork();
  2. if (fd == 0)
  3. {
  4. //child
  5. close(_listensock);
  6. if (fork() > 0)
  7. {
  8. exit(0);
  9. }
  10. //孙子进程
  11. Server(serverSocket, addr);
  12. close(serverSocket);
  13. exit(0);
  14. }
  15. else if (fd > 0)
  16. {
  17. //father
  18. close(serverSocket);
  19. waitpid(fd, nullptr, 0);
  20. }
  21. else
  22. {
  23. std::cout << "fork error" << std::endl;
  24. }

创建子进程之后,子进程会再创建一个孙子进程,子进程创建完孙子进程之后就退出,让孙子进程变成孤儿进程,从而被操作系统领养。此后,而父进程创建完子进程之后就开始等待子进程退出,子进程创建完孙子进程就退出,所以父进程并不需要等待很长的时间。

这种方式也是可以成功运行的。

确实创建了两个进程,并且这两个进程的ppid是1(被bash领养了),当客户端退出的时候,操作系统会自动释放孤儿进程的资源

4.1.3信号捕捉

在学习系统的信号部分时,我们学过当子进程退出时会发送SIGCHLD信号,信号的主要作用是通知父进程其一个子进程已经终止,以便父进程可以执行一些清理工作,比如回收子进程的资源。

父进程只要捕捉这个信号,并将这个信号忽略即可。子进程会自动释放资源。

  1. signal(SIGCHLD, SIG_IGN);
  2. // V3多进程信号版
  3. int fd = fork();
  4. if (fd == 0)
  5. {
  6. // child
  7. close(_listensock);
  8. Server(serverSocket, addr);
  9. close(serverSocket);
  10. exit(0);
  11. }
  12. else if (fd > 0)
  13. {
  14. // father
  15. close(serverSocket);
  16. }

也是可以正常运行的。

也是正常的三个进程在运行。接下来我们将两个子进程退出看看

最后只剩下父进程在跑了,而且子进程也没有显示出僵尸状态

4.2多线程

虽然使用多进程的方式可以解决问题,但是多进程的方式并不是主流,原因是多进程的创建成本比较大,每个进程独享一个进程地址空间,页表,进程控制块,文件描述符表等。实际开发中,我们会更倾向于选择使用多线程的方式。

只需要让主线程负责获取新连接,子线程负责给用提供服务(读取用户的信息,并将信息经过处理后,再返回给用户)。

因为子线程和主线程共享一个文件描述符表,所以并不像多进程那样双方都需要关闭文件描述符,只需要子线程在服务完之后关闭文件描述符即可。

线程等待问题

和进程一样,我们创建完线程之后,也是需要等待线程的,但是也有方法可以不用进行等待,那就是线程分离(pthread_detach),线程如果分离了,会在退出时自动释放资源。


有了大概思路之后我们就可以开始实现了

  1. struct ThreadData
  2. {
  3. ThreadData(int sockfd, sockaddr_in user, TcpServer *self)
  4. : _sockfd(sockfd), _user(user), _self(self)
  5. {
  6. }
  7. public:
  8. int _sockfd;
  9. sockaddr_in _user;
  10. TcpServer *_self;
  11. };

先创建一个线程传递的参数,用于线程将来调用Server函数。这个参数是一个结构体,内部有三个对象,sockfd告诉线程从哪个文件描述符中读,user告诉线程客户端的信息(IP,Port),剩下一个参数等会说

  1. pthread_t p;
  2. ThreadData *data = new ThreadData(serverSocket, addr, this);
  3. pthread_create(&p, nullptr, Handler, (void *)data);

我们可以开始创建一个线程了,但是pthread_create的第三个参数是一个返回值为void*,参数也为void*的函数指针,如果传的是类内成员函数Handler,就会报错

因为类内成员函数会自带一个隐藏的this指针,为了解决这个办法,我们只能将Handler函数设置为static

  1. static void* Handler(void* arg)
  2. {
  3. }

但是这时,新的问题又出现了,Handler是静态的,无法调用类内成员函数Server了,所以我们就给线程传递的参数中增加了第三个对象,一个自己类型的指针。

  1. static void *Handler(void *arg)
  2. {
  3. pthread_detach(pthread_self());
  4. ThreadData *data = static_cast<ThreadData *>(arg);
  5. data->_self->Server(data->_sockfd, data->_user)
  6. }

先将线程进行分离,然后将arg参数强转为ThreadData类型,然后通过我们之前传递的this指针来调用Server函数。

  1. void Start()
  2. {
  3. signal(SIGCHLD, SIG_IGN);
  4. while (true)
  5. {
  6. // 获取新连接
  7. struct sockaddr_in addr;
  8. socklen_t addrlen = sizeof(addr);
  9. memset(&addr, 0, sizeof(addr));
  10. int serverSocket = accept(_listensock, (sockaddr *)&addr, &addrlen);
  11. pthread_t p;
  12. ThreadData *data = new ThreadData(serverSocket, addr, this);
  13. pthread_create(&p, nullptr, Handler, (void *)data);
  14. }
  15. }
  16. static void *Handler(void *arg)
  17. {
  18. pthread_detach(pthread_self());
  19. ThreadData *data = static_cast<ThreadData *>(arg);
  20. data->_self->Server(data->_sockfd, data->_user);
  21. close(data->_sockfd);
  22. }

测试:

也是可以成功运行的。

创建了三个线程(PID都相同)。

并且查看当前进程,可以看到创建了三个文件描述符,3为主线程socket获取的,其他两个是accept获得的。

当用户退出时,也可以正常关闭文件描述符。

4.3线程池版本

刚刚我们选的多线程版本还存在问题吗?答案是还存在的。

每次用户发起连接后,才会创建线程,并且在断开连接,会释放线程,这样反复创建释放,效率就会比较低

如何解决这个问题呢,我们需要使用线程池来解决。线程池就是会预先创建一批线程,这些线程会进入休眠状态,等到有用户连接时,会将一个线程唤醒,然后让这个线程去为用户提供服务。

准备工作

首先我们需要一个线程池

  1. template <class T>
  2. class ThreadPool
  3. {
  4. private:
  5. ThreadPool(size_t thread_num = 5, size_t task_num = 10)
  6. : _thread_num(thread_num), _task_num(task_num)
  7. {
  8. pthread_mutex_init(&_mutex, nullptr);
  9. pthread_cond_init(&_cond, nullptr);
  10. for (int i = 0; i < _thread_num; i++)
  11. {
  12. std::string thread_name = GetPthreadName();
  13. _threads.push_back(Thread<void, std::string>(std::bind(&ThreadPool::ThreadRun, this, std::placeholders::_1), thread_name, thread_name));
  14. log.LogMessage(Debug, "create %s success", thread_name.c_str());
  15. }
  16. }
  17. ThreadPool(const ThreadPool<T> &tp) = delete;
  18. const ThreadPool<T> &operator=(const ThreadPool<T> &tp) = delete;
  19. public:
  20. static ThreadPool *GetInstance()
  21. {
  22. if (_instance == nullptr)
  23. {
  24. LockGuard lg(&_mtinst);
  25. if (_instance == nullptr)
  26. {
  27. _instance = new ThreadPool<T>();
  28. }
  29. }
  30. return _instance;
  31. }
  32. ~ThreadPool()
  33. {
  34. pthread_mutex_destroy(&_mutex);
  35. pthread_cond_destroy(&_cond);
  36. }
  37. void ThreadRun(std::string name)
  38. {
  39. while (true)
  40. {
  41. T t;
  42. {
  43. LockGuard lg(&_mutex);
  44. while (_task_queue.empty())
  45. {
  46. ThreadWait(name);
  47. }
  48. t = _task_queue.front();
  49. _task_queue.pop();
  50. }
  51. t();
  52. }
  53. }
  54. void Start()
  55. {
  56. for (auto &th : _threads)
  57. {
  58. th.Create();
  59. }
  60. }
  61. void Wait()
  62. {
  63. for (auto &th : _threads)
  64. {
  65. th.Jion();
  66. }
  67. }
  68. void Push(T date)
  69. {
  70. LockGuard lock(&_mutex);
  71. _task_queue.push(date);
  72. ThreadWakeUp();
  73. }
  74. private:
  75. void ThreadWait(const std::string &thread_name)
  76. {
  77. log.LogMessage(Debug, "%s start to wait", thread_name.c_str());
  78. pthread_cond_wait(&_cond, &_mutex);
  79. }
  80. void ThreadWakeUp()
  81. {
  82. pthread_cond_signal(&_cond);
  83. }
  84. private:
  85. std::vector<Thread<void, std::string>> _threads;
  86. std::queue<T> _task_queue;
  87. size_t _thread_num;
  88. size_t _task_num;
  89. pthread_mutex_t _mutex;
  90. pthread_cond_t _cond;
  91. static ThreadPool<T> *_instance;
  92. static pthread_mutex_t _mtinst;
  93. Log log;
  94. };
  95. template <class T>
  96. ThreadPool<T> *ThreadPool<T>::_instance = nullptr;
  97. template <class T>
  98. pthread_mutex_t ThreadPool<T>::_mtinst = PTHREAD_MUTEX_INITIALIZER;

先完成线程池的代码,该线程池会自动创建5个线程,并且会绑定ThreadRun函数,然后我们使用你start函数就可以启动线程池,5个线程会被启动,开始执行ThraedRun函数,并被阻塞住,我们只需要将任务push到_task_queue任务队列当中即可,当任务push之后,会唤醒一个线程,并让这个线程去执行刚刚push的任务。

注意:上面有一个LogMessage函数,主要就是用于打印日志,方便我们观看,还有一个LockGuard函数,我们将锁交给他,他会帮我们管理这个锁,出了作用域之后自动释放。下面是这个函数的原型。

  1. #pragma once
  2. #include <iostream>
  3. #include <pthread.h>
  4. class Mutex;
  5. class LockGuard;
  6. class Mutex
  7. {
  8. public:
  9. Mutex(pthread_mutex_t *lock)
  10. :_lock(lock)
  11. {}
  12. void Lock()
  13. {
  14. pthread_mutex_lock(_lock);
  15. }
  16. void Unlock()
  17. {
  18. pthread_mutex_unlock(_lock);
  19. }
  20. ~Mutex()
  21. {}
  22. private:
  23. pthread_mutex_t *_lock;
  24. };
  25. class LockGuard
  26. {
  27. public:
  28. LockGuard(pthread_mutex_t* mutex)
  29. : _mutex(mutex)
  30. {
  31. _mutex.Lock();
  32. }
  33. ~LockGuard()
  34. {
  35. _mutex.Unlock();
  36. }
  37. private:
  38. Mutex _mutex;
  39. };

封装用户信息

我们前面使用sockaddr_in来保存用户端发来的信息,例如IP和端口号,但是需要我们手动写,将sockaddr_in中的信息提取出来,比较麻烦,我们可以封装成一个类

  1. class InetAddr
  2. {
  3. public:
  4. InetAddr(const sockaddr_in addr)
  5. : _addr(addr)
  6. {
  7. _ip = inet_ntoa(addr.sin_addr);
  8. _port = ntohs(addr.sin_port);
  9. }
  10. std::string GetUser()
  11. {
  12. std::string temp;
  13. temp += _ip;
  14. temp += " : ";
  15. temp += std::to_string(_port);
  16. return temp;
  17. }
  18. std::string& GetIp()
  19. {
  20. return _ip;
  21. }
  22. uint16_t GetPort()
  23. {
  24. return _port;
  25. }
  26. sockaddr_in& GetAddr()
  27. {
  28. return _addr;
  29. }
  30. private:
  31. std::string _ip;
  32. uint16_t _port;
  33. sockaddr_in _addr;
  34. };

我们只需要将sockaddr_in交给这个函数,他就会自动帮我们提取IP和端口号,并且GetUser函数还会返回IP+端口的一个字符串

线程池版本整体框架

现在完事具备了,可以正式开始写服务了

  1. TcpServer(uint16_t &port, std::string ip = "")
  2. {
  3. // 创建套接字
  4. _listensock = socket(AF_INET, SOCK_STREAM, 0);
  5. if (_listensock < 0)
  6. {
  7. lg.LogMessage(Error, "create socket error");
  8. exit(1);
  9. }
  10. lg.LogMessage(Normal, "create socket success, socket:%d", _listensock);
  11. int opt = 1;
  12. setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
  13. // 绑定IP和端口
  14. struct sockaddr_in addr;
  15. addr.sin_family = AF_INET;
  16. addr.sin_port = htons(port);
  17. addr.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());
  18. if (bind(_listensock, (sockaddr *)&addr, sizeof(addr)) < 0)
  19. {
  20. lg.LogMessage(Error, "bind error");
  21. exit(2);
  22. }
  23. lg.LogMessage(Normal, "bind success");
  24. // 监听listen
  25. if (listen(_listensock, backlog) < 0)
  26. {
  27. lg.LogMessage(Error, "listen error");
  28. exit(3);
  29. }
  30. lg.LogMessage(Normal, "listen success");
  31. ThreadPool<task_t>::GetInstance()->Start();
  32. }

首先就是我们要在构造函数中添加,让线程池启动。

  1. using task_t = std::function<void()>;
  2. using callback_t = std::function<void(std::string&)>;

然后使用function,设计将来我们会将什么函数push进线程池内。

  1. void Start()
  2. {
  3. while (true)
  4. {
  5. struct sockaddr_in addr;
  6. socklen_t addrlen = sizeof(addr);
  7. memset(&addr, 0, sizeof(addr));
  8. int serverSocket = accept(_listensock, (sockaddr *)&addr, &addrlen);
  9. // 从这往后,都使用serverSocket
  10. InetAddr user(addr);
  11. // V5线程池版
  12. task_t task = std::bind(&TcpServer::Routine, this, serverSocket, user);
  13. ThreadPool<task_t>::GetInstance()->Push(task);
  14. }
  15. }

每接受到一个用户申请的连接后,我们就创建一个task_t的函数对象,并且绑定一下对应的函数以及其参数,再将这个函数push到线程池内,push后自动唤醒一个线程去执行这个函数。

接下来我们只需要完成Routine函数的逻辑即可。这个函数我们可以设计成之前的Server

  1. void Server(int serverSocket, InetAddr user)
  2. {
  3. sockaddr_in addr = user.GetAddr();
  4. socklen_t addrlen = sizeof(addr);
  5. char temp[1024];
  6. while (true)
  7. {
  8. std::string buffer = "[" + user.GetUser() + "] ";
  9. int n = recvfrom(serverSocket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
  10. if (n > 0)
  11. {
  12. temp[n] = 0;
  13. buffer += temp;
  14. sendto(serverSocket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
  15. }
  16. else if (n == 0)
  17. {
  18. std::cout << "you close the connect, me too" << std::endl;
  19. break;
  20. }
  21. else
  22. {
  23. std::cout << "recvfrom error" << std::endl;
  24. break;
  25. }
  26. }
  27. close(serverSocket);
  28. }

读取用户消息,然后处理(这里的处理是在消息前面加上客户端的IP和端口),再重新发送给用户。

我们先使用Server测试一下逻辑有无问题。

启动服务端之后,帮我们创建了五个线程,并且这五个线程都进入阻塞等待

最后也是符合我们的预期。

如果我们想完成更多功能呢,用户启动的时候,我们给他发送能提供哪些服务,根据用户的选择,执行不同的服务。

为了达到刚刚的目标,首先我们需要一些函数

提供各种业务

  1. void Ping(std::string& str)
  2. {
  3. str = "Pong";
  4. }

如果用户选择Ping函数,无论用户发送什么我们都给他发送一个Pong。

  1. void Transforme(std::string& str)
  2. {
  3. for (auto& ch : str)
  4. {
  5. if ('a' <= ch && ch <= 'z')
  6. {
  7. ch -= 32;
  8. }
  9. }
  10. }

Transforme函数将用户传入的字符串全部改为大写字母。

  1. void Translate(std::string& str)
  2. {
  3. std::string chinese = dict.Find(str);
  4. str = chinese;
  5. }

Translate函数会将用户传过来的英文转化成中文返还给用户。

我们先从网上找一些中英文对照的单词。并将这些单词保存在一个文件当中

我们要做的是封装一个类,将这个文件中的所有单词读取出来(按行读),然后将每一行单词和对应的中文保存起来。

  1. #include <iostream>
  2. #include <cstring>
  3. #include <string>
  4. #include <fstream>
  5. #include <vector>
  6. #include <unordered_map>
  7. const std::string path = "./word/E_C.txt";
  8. class Dictionary
  9. {
  10. public:
  11. Dictionary()
  12. {
  13. Set();
  14. }
  15. void Updata()
  16. {
  17. lines.clear();
  18. dict.clear();
  19. Set();
  20. }
  21. void Set()
  22. {
  23. std::ifstream file(path);
  24. if (!file.is_open())
  25. {
  26. std::cerr << "打开文件失败" << std::endl;
  27. exit(1);
  28. }
  29. std::string buffer;
  30. while (std::getline(file, buffer))
  31. {
  32. lines.push_back(buffer);
  33. //读取空行
  34. std::getline(file, buffer);
  35. }
  36. //切分
  37. for (auto& line : lines)
  38. {
  39. size_t left = line.find(" ");
  40. //真正的单词是从left + 1开始找的
  41. size_t right = line.find(" ", left + 1);
  42. dict.insert(std::make_pair(line.substr(left + 1, right - left - 1), line.substr(right + 1)));
  43. }
  44. }
  45. std::string Find(std::string word)
  46. {
  47. auto it = dict.find(word);
  48. if (it == dict.end())
  49. {
  50. return "Not found";
  51. }
  52. else
  53. {
  54. return it->second;
  55. }
  56. }
  57. private:
  58. std::vector<std::string> lines;
  59. std::unordered_map<std::string, std::string> dict;
  60. };

Set函数会执行我们刚刚说的那几步,按行读取文本,然后切分并保存进dict中,未来我们只需要调用Find函数就能获取英文对应的汉语。 

我们在TcpServer中创建一个函数名和函数的映射的哈希表。

std::unordered_map<std::string, callback_t> _funcs;

我给TcpServer中添加一个函数,用于添加新的函数。

  1. void AddFunc(std::string func_name, callback_t task)
  2. {
  3. _funcs.insert({func_name, task});
  4. }

有了这几个函数之后,我们可以将这几个函数交给TcpServer

  1. t->AddFunc("ping", Ping);
  2. t->AddFunc("transforme", Transforme);
  3. t->AddFunc("translate", Translate);

Routinue函数

我们想让这个函数达到的效果就是,将来一个用户申请连接之后,我们就让一个线程去执行这个函数。

  • 1.先给用户发送有哪些服务
  1. // 向用户发送有哪些功能
  2. std::string str = GetAllFunction();
  3. Sendto(sockfd, user, str);

GetAllFunct函数就是将_funcs中的所有函数的函数名形成一个字符串。然后发送给字符串。

  • 2.读取用户选择
  1. std::string buffer;
  2. Recvfrom(sockfd, buffer);

将用户想要的选择读取到buffer中。

  • 3.读取用户的输入
  1. std::string message;
  2. Recvfrom(sockfd, message);

读取用户对相应服务的输入信息

  • 4.执行用户选择服务
  1. // 根据选择执行相应的方法
  2. auto func = _funcs.find(buffer);
  3. if (func != _funcs.end())
  4. {
  5. func->second(message);
  6. }
  7. else
  8. {
  9. message = "choise the tast not exist";
  10. }

buffer中就是客户选择的服务,直接去func中查询。

  • 5.将数据写回
  1. //将数据写回
  2. Sendto(sockfd, user, message);
  3. close(sockfd);

再将数据写回,再将sockfd写回去。

长连接问题

我们之前写的服务都是长连接(连接之后死循环)。它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接的缺点就是,连接数过多时,影响服务端的性能和并发数量

所以我们尽量使用短链接的方式,一次连接就完成了

通常浏览器访问服务器的时候就是短连接。对于服务端来说,长连接会耗费服务端的资源,而且用户用浏览器访问服务端相对而言不是很频繁的如果有几十万,上百万的连接,服务端的压力会非常大,甚至会崩溃。所以对于并发量大,请求频率低的,建议使用短连接

客户端

  1. #include <iostream>
  2. #include <sys/types.h>
  3. #include <sys/socket.h>
  4. #include <netinet/in.h>
  5. #include <arpa/inet.h>
  6. #include "LogMessage.hpp"
  7. void Usage()
  8. {
  9. std::cout << "Please enter: ./Client ip port" << std::endl;
  10. }
  11. int main(int argc, char *argv[])
  12. {
  13. if (argc != 3)
  14. {
  15. Usage();
  16. return 4;
  17. }
  18. std::string ip = argv[1];
  19. uint16_t port = atoi(argv[2]);
  20. Log lg;
  21. // 1.创建套接字
  22. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  23. if (sockfd < 0)
  24. {
  25. lg.LogMessage(Error, "create socket error");
  26. exit(1);
  27. }
  28. lg.LogMessage(Normal, "create socket success, socket:%d", sockfd);
  29. // 2.发起连接
  30. sockaddr_in addr;
  31. socklen_t addrlen = sizeof(addr);
  32. addr.sin_family = AF_INET;
  33. addr.sin_port = htons(port);
  34. addr.sin_addr.s_addr = inet_addr(ip.c_str());
  35. if (connect(sockfd, (sockaddr *)&addr, addrlen) < 0)
  36. {
  37. lg.LogMessage(Error, "connect error");
  38. exit(2);
  39. }
  40. lg.LogMessage(Normal, "connect success");
  41. // 3.先接受有哪些服务
  42. char temp[128];
  43. sockaddr_in raddr;
  44. socklen_t raddrlen = sizeof(addr);
  45. int n = recvfrom(sockfd, temp, sizeof(temp) - 1, 0, (sockaddr *)&raddr, &raddrlen);
  46. if (n <= 0)
  47. {
  48. std::cout << "recvfrom error" << std::endl;
  49. exit(3);
  50. }
  51. temp[n] = 0;
  52. std::cout << temp << std::endl;
  53. // 4.向服务端发送你想要执行的服务
  54. std::string buffer;
  55. std::cout << "Please enter# " << std::endl;
  56. getline(std::cin, buffer);
  57. sendto(sockfd, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, addrlen);
  58. // 5.为你要执行的服务发送消息
  59. std::cout << "Please enter you message: " << std::endl;
  60. getline(std::cin, buffer);
  61. sendto(sockfd, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, addrlen);
  62. // 6.接受处理后的信息
  63. raddrlen = sizeof(addr);
  64. n = recvfrom(sockfd, temp, sizeof(temp) - 1, 0, (sockaddr *)&raddr, &raddrlen);
  65. if (n > 0)
  66. {
  67. temp[n] = 0;
  68. std::cout << temp << std::endl;
  69. }
  70. return 0;
  71. }

运行程序

我们发先客户端在执行完一次服务之后就退出了,而服务端在执行完服务之后,线程也阻塞挂起了。

执行transforme之后,也会将用户的信息转化成大写。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/467235
推荐阅读
相关标签
  

闽ICP备14008679号