• Linux---Socket


    网络套接字(socket)

    网络通信仅仅是为了让两台主机间传送数据吗?数据是被谁需要的呢?--- 进程,所以网络通信的本质是两个进程间的通信。那么如何找到两台主机上的两个进程呢?

    1、通过IP地址确定网络中的唯一一台主机

    2、通过port(端口号)确定一台主机上的唯一一个进程(端口号是一个2字节16位的整数)

    IP和port 就是 网络socket(套接字),由此可以标识互联网中的唯一一个进程

    如何理解端口号(port)

    (端口号和进程是对应的,通过端口号来找到进程id)

    我们知道操作系统中可以通过pid来标识进程,为什么还要有端口号呢?

    1、将其他模块(进程管理)和网络进行解耦

    2、端口号是专门为网络服务的

    一个进程可以有多个端口号,但是一个端口号只能对应一个进程。

    TCP和UDP协议特点

    TCP:1、传输层协议    2、有连接    3、可靠传输       4、面向字节流

    UDP:1、传输层协议    2、无连接    3、不可靠传输   4、面向数据报

    (注意:这里说的可靠不可靠,没有褒贬之分,是协议的特性,即TCP在传输数据时会考虑数据出错 / 丢失的问题,但是UDP不会,所以TCP会更慢,UDP会更快,各有各的特性)

    网络字节序

    大家在学C语言的时候就应该了解过电脑有大小端之分(大端:高位放在低地址处,小端:低位放在低地址处,是两种不同的数据存储方式)。本来在一台主机上没什么问题,但是网络通信发生在大端机和小端机之间,那么我们解释数据就会出问题,如下

    为了方便我们进行数据的大小端转化,系统还提供了一些接口

    (直接用就行,不用我们再去关心机器是大端还是小端) 

    socket编程接口

     接口的具体使用,会在后面介绍

    一、UDP协议网络编程

    (这里暂且不去关心UDP的底层原理,先来熟悉一下UDP的相关接口)

    1、接口介绍

    int socket(int domain, int type, int protocol);
    • domain:指定通信协议族,例如AF_INET表示IPv4协议,AF_INET6表示IPv6协议,AF_UNIX表示本地通信(Unix域套接字)等。
    • type:指定套接字类型,常见的类型有:
      • SOCK_STREAM:提供流式套接字,用于TCP协议。
      • SOCK_DGRAM:提供数据报套接字,用于UDP协议。
      • SOCK_RAW:提供原始套接字,允许对底层协议(如IP或ICMP)进行直接访问。
    • protocol:通常设置为0,表示选择默认的协议。在大多数情况下,内核可以根据domaintype参数自动确定所使用的协议。

    函数成功返回套接字的文件描述符,失败返回-1

    int sockfd = socket(AF_INET,SOCK_DGRAM,0); // UDP默认固定写法
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
    • sockfd:这是由socket函数返回的套接字文件描述符。
    • addr:这是一个指向sockaddr结构体的指针,该结构体包含了要绑定的地址信息,包括IP地址和端口号。对于IPv4地址,通常使用sockaddr_in结构体;对于IPv6地址,使用sockaddr_in6结构体。
    • addrlen:这是addr参数所指向的结构体的长度,通常以字节为单位。这可以用来确定结构体中包含了哪些字段,以及这些字段的长度。

    功能描述

    bind函数将sockfd所指定的套接字与addr参数所指向的地址结构体进行绑定。这允许服务器在特定的IP地址和端口号上监听客户端的连接请求。如果addr中的地址是通配符地址(如IPv4中的INADDR_ANY),则套接字将绑定到所有可用的网络接口上。

    返回值

    • 如果绑定成功,bind函数返回0。
    • 如果绑定失败,bind函数返回-1,并设置全局变量errno以指示错误原因。

    注意事项

    • 在调用bind函数之前,通常需要先调用socket函数来创建一个套接字。
    • bind函数通常用于服务器端套接字,以指定服务器应该在哪个地址和端口上监听连接请求。客户端套接字通常不需要调用bind函数,因为它们会由操作系统自动分配一个本地端口号。
    • 在某些情况下,如果套接字已经与某个地址绑定,再次调用bind函数可能会失败。此外,如果指定的地址或端口号已经被其他套接字使用,或者是不合法的,bind函数也会失败。
    1. ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
    2. struct sockaddr *src_addr, socklen_t *addrlen);
    • sockfd:指定接收数据的套接字文件描述符。
    • buf:指向用于存储接收数据的缓冲区。
    • len:指定缓冲区的最大长度,即最多可以接收多少字节的数据。
    • flags:控制接收数据的方式。例如,MSG_WAITALL标志可以确保在函数返回之前,指定长度的数据全部被接收;MSG_DONTWAIT标志则使函数以非阻塞方式工作,如果没有数据可读,它将立即返回。默认设置为0
    • src_addr:指向一个sockaddr结构体(或其特定类型,如sockaddr_in对于IPv4)的指针,该结构体在函数返回时将被填充发送方的地址信息。
    • addrlen:是一个指向socklen_t变量的指针,用于传入src_addr结构体的初始长度,并在函数返回时更新为实际填充的长度。

    recvfrom函数的返回值是实际接收到的字节数。如果返回值为0,表示连接已关闭。如果出现错误,返回值为-1,此时可以通过errno变量来获取具体的错误信息。

    1. ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
    2. const struct sockaddr *dest_addr, socklen_t addrlen);
    • sockfd:是已经创建好的socket文件描述符,代表要发送数据的套接字。
    • buf:指向包含待发送数据的缓冲区的指针。
    • len:指定缓冲区中数据的长度,即要发送的字节数。
    • flags:发送选项,用于控制发送数据的方式。通常情况下,这个参数设置为0即可。
    • dest_addr:指向包含目的地址信息的sockaddr结构体(或其特定类型,如sockaddr_in对于IPv4)的指针。
    • addrlen:指定dest_addr结构体的大小。

    返回值:

    • 如果发送成功,sendto函数返回发送的字节数。
    • 如果发送失败,返回-1,并设置全局变量errno以指示错误原因。

    使用sendto函数时,通常不需要事先与目的主机建立连接,因此它非常适合于实现无连接的数据传输服务,如UDP。同时,由于发送和接收数据报是独立的,所以sendto函数允许数据报以任意顺序到达,并且不保证数据报的可靠传输。

    下面是一个简单用UDP实现的将客户端发来的消息再给它发回去的代码

    2、服务端代码

    1. // Udp_Server.hpp
    2. #include
    3. #include
    4. #include
    5. // 网络相关的头文件
    6. #include
    7. #include
    8. #include
    9. #include
    10. // 自定义的头文件 --- 在文章的结尾 - 附录部分 - 有具体的代码实现,有兴趣可以看看
    11. // (这些头文件对我们理解代码的核心逻辑没有太大影响,只是为了让编码更加有条理)
    12. #include "nocopy.hpp" // 包含防拷贝和赋值的类
    13. #include "Log.hpp" // 包含日志类
    14. #include "Comm.hpp" // 包含一些公用的变量
    15. #include "InetAddr.hpp" // 包含管理网络地址的类
    16. uint16_t portdefault = 8888;
    17. class UdpServer : public nocopy // 继承防拷贝类,让自己也防拷贝
    18. {
    19. public:
    20. UdpServer(uint16_t port = portdefault)
    21. : _port(port)
    22. {}
    23. void Init()
    24. {
    25. // 创建套接字
    26. _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    27. if (_sockfd == -1)
    28. {
    29. lg(Fatal, "socket faild : %d - %s", errno, strerror(errno)); // 打印日志,可以理解为cout,是我们单独封装的日志类
    30. exit(SocketErr);
    31. }
    32. lg(Info, "socket success, sockfd: %d", _sockfd);
    33. // 绑定 bind
    34. // 1、填充结构体
    35. struct sockaddr_in local;
    36. memset(&local, 0, sizeof(local));
    37. local.sin_family = AF_INET;
    38. local.sin_port = htons(_port);
    39. local.sin_addr.s_addr = INADDR_ANY; // 绑定本机任意ip地址,可以收到发送到该主句的该端口号上的所有请求
    40. // 因为一台主机可能有多个ip地址,如果绑死了,就只能接收到ipx这个地址收到请求,发送到其他的ip地址的请求就收不到了!!!
    41. // 2、进行绑定
    42. int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
    43. if (n != 0)
    44. {
    45. lg(Fatal, "bind faild : %d - %s", errno, strerror(errno));
    46. exit(BindErr);
    47. }
    48. lg(Info, "bind success");
    49. }
    50. void Start()
    51. {
    52. char buffer[1024];
    53. for (;;)
    54. {
    55. struct sockaddr_in peer;
    56. socklen_t len = sizeof(peer);
    57. ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
    58. if (n > 0)
    59. {
    60. InetAddr addr(peer); // 这是用来管理发送方的ip和port的类,也是我们自己封装的
    61. buffer[n] = 0;
    62. std::cout << "[" << addr.DebugPrinit() << "]#" << buffer << std::endl;
    63. sendto(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, len);
    64. }
    65. }
    66. }
    67. ~UdpServer()
    68. {}
    69. private:
    70. uint16_t _port;
    71. int _sockfd;
    72. };
    73. // UdpServer.cpp
    74. #include "Udp_Sercer.hpp"
    75. #include
    76. #include
    77. void Usage(std::string commond)
    78. {
    79. std::cout << "Usage:\n";
    80. std::cout << commond << " port" << std::endl;
    81. }
    82. int main(int argc, char *args[])
    83. {
    84. if (argc != 2)
    85. {
    86. Usage(args[0]);
    87. exit(1);
    88. }
    89. uint16_t port = std::stoi(args[1]);
    90. std::unique_ptr s(new UdpServer(port));
    91. s->Init();
    92. s->Start();
    93. return 0;
    94. }

    3、客户端代码

    1. #include
    2. #include
    3. // 网络相关的头文件
    4. #include
    5. #include
    6. #include
    7. #include
    8. // 自定义的头文件
    9. #include "Log.hpp"
    10. #include "Comm.hpp"
    11. void Usage(std::string commond)
    12. {
    13. std::cout << commond << " server_ip server_port" << std::endl;
    14. }
    15. int main(int argc, char *args[])
    16. {
    17. if (argc != 3)
    18. {
    19. Usage(args[0]);
    20. exit(1);
    21. }
    22. uint16_t port = std::stoi(args[2]);
    23. // 创建socket
    24. int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    25. if (sockfd < 0)
    26. {
    27. lg(Fatal, "socket faild : %d - %s", errno, strerror(errno));
    28. exit(SocketErr);
    29. }
    30. // 客户端需要bind吗?需要,但是我们不用显示的bind,这样也不好
    31. // 因为客户端会有多个,每个人的ip地址和端口号都是动态变化的,一旦绑死,就会出问题
    32. // 当我们第一次发送数据时,本机OS会自动帮我们bind上随机对应的ip地址和port
    33. // 填充服务器的网络地址
    34. struct sockaddr_in server;
    35. memset(&server, 0, sizeof(server));
    36. server.sin_family = AF_INET;
    37. server.sin_port = htons(port); // 注意转换成网络字节序
    38. server.sin_addr.s_addr = inet_addr(args[1]); // inet_addr()将字符串转换成4字节的整形,并变成网络字节序,系统提供的函数
    39. //sockfd支持全双工通信
    40. while (true)
    41. {
    42. std::cout << "please enter# ";
    43. std::string buffer;
    44. std::getline(std::cin, buffer);
    45. ssize_t n = sendto(sockfd, buffer.c_str(), buffer.size(), 0, (struct sockaddr *)&server, sizeof(server));
    46. if (n > 0)
    47. {
    48. char buffer[1024];
    49. struct sockaddr_in tmp;
    50. socklen_t len = sizeof(tmp);
    51. ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&tmp, &len); // 虽然我们知道服务器的网络地址信息,但是后面两个参数一般都要传,传nullptr可能会出错
    52. if (m > 0)
    53. {
    54. buffer[m] = 0;
    55. std::cout << "server recv# " << buffer << std::endl;
    56. }
    57. else
    58. break;
    59. }
    60. else
    61. break;
    62. }
    63. close(sockfd);
    64. return 0;
    65. }

    当我们熟悉了这些接口,我们就可以将代码进行改造,实现一些其他的有趣的功能,比如多人聊天会议室(需要结合多线程),远程控制自己的服务器等等。

    二、TCP协议网络编程

    1、接口介绍

    1. int socket(int domain, int type, int protocol);
    2. // 和UDP中介绍的socket一样
    3. // 在选择tcp协议时,参数选项上有所差异,如下
    4. int sockfd = socket(AF_INET, SOCK_STREAM, 0); // tcp 默认固定写法
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    用法和上面的UDP一样

    int listen(int sockfd, int backlog);
    • sockfd:这是listen函数的操作对象,它是建立socket时的返回值,即已绑定但未连接的套接字描述符。
    • backlog:这个参数代表了TCP连接请求的最大队列长度。具体来说,它指定了内核接受客户端SYN但未完成三次握手的TCP连接数量。如果连接请求的数量超过了backlog设置的值,后续的连接请求将会被拒绝。backlog参数的设置越大,TCP服务器可以同时处理的客户端连接就越多,但请注意,backlog的值应大于2,否则listen函数会失败。

    函数返回值:如果listen函数调用成功,则返回0;如果失败,则返回-1,并可以通过errno获取错误编号。

    注意:listen函数通常在调用bind函数之后和调用accept函数之前被调用。当listen函数调用成功后,TCP服务端的状态就会变为“LISTEN”,等待客户端的连接请求。 

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    • sockfd:这是accept函数的操作对象,它通常是一个处于监听状态的套接字描述符,由socket函数创建并经过bind和listen函数处理后的套接字。
    • addr:这是一个指向sockaddr结构的指针,用于存储客户端的地址信息。当accept函数成功接受一个连接后,它会把客户端的IP地址和端口号等信息写入这个结构中。
    • addrlen:这是一个指向socklen_t类型变量的指针,用于存储addr结构体的实际长度。在调用accept函数前,通常会把这个变量的值设置为addr结构体的大小,函数返回时,它会被更新为实际写入的长度。

    函数返回值:

    • 如果accept函数成功接受了一个连接,它会返回一个新的套接字描述符,这个描述符用于和客户端进行通信。原来的sockfd套接字则继续用于监听其他客户端的连接请求。
    • 如果accept函数失败,则返回-1,并设置全局变量errno以表示错误。

    值得注意的是,当sockfd套接字处于监听状态时,它会等待客户端的连接请求。一旦有客户端连接请求到来,accept函数会从已完成连接队列中取出一个连接请求,并创建一个新的套接字与该客户端通信。同时,accept函数会阻塞当前线程,直到有连接请求到来或者发生错误。

    1. int connect(int sockfd, const struct sockaddr *addr,
    2. socklen_t addrlen);
    • sockfd:这是标识一个套接字的文件描述符。它是由先前的socket函数调用返回的值,代表了客户端想要建立连接的套接字。
    • serv_addr:这是一个指向sockaddr结构的指针,包含了客户端想要连接的主机的地址和端口号信息。这个参数指定了数据发送的目的地,即服务器的地址。
    • addrlen:这是一个socklen_t类型的变量,表示serv_addr参数所指向的地址结构的长度。它帮助函数正确解析serv_addr中的地址信息。

    返回值

    • 成功:当connect函数成功建立连接时,返回0。
    • 失败:如果连接建立失败,connect函数返回-1,并将错误原因存储在全局变量errno中。可能的错误原因包括但不限于:
      • EBADF:参数sockfd不是一个合法的socket处理代码。
      • EFAULT:参数serv_addr指针指向无法存取的内存空间。
      • ENOTSOCK:参数sockfd是一个文件描述词,但它不是一个socket。
      • EISCONN:参数sockfd的socket已经是连线状态。
      • ECONNREFUSED:连线要求被server端拒绝。
      • ETIMEDOUT:企图连线的操作超过限定时间仍未有响应。
      • ENETUNREACH:无法传送数据包至指定的主机。
      • EAFNOSUPPORT:sockaddr结构的sa_family不正确。
      • EALREADY:socket为不可阻塞且先前的连线操作还未完成。

    这里可以通过read和write两个文件相关的系统调用进行对tcp套接字的读写工作

    2、服务端代码

    1. // TcpServer.hpp
    2. #pragma once
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include
    13. #include "nocopy.hpp"
    14. #include "Log.hpp"
    15. #include "Comm.hpp"
    16. #include "InetAddr.hpp"
    17. #include "ThreadPool.hpp"
    18. const int DefaultBacklog = 5;
    19. class TcpServer : public nocopy
    20. {
    21. public:
    22. TcpServer(uint16_t port) : _port(port), _isrunning(false)
    23. {
    24. }
    25. void Init()
    26. {
    27. // 创建套接字
    28. _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
    29. if (_listensockfd < 0)
    30. {
    31. lg(Fatal, "socket failed");
    32. exit(SocketErr);
    33. }
    34. lg(Info, "socket success, listensockfd: %d", _listensockfd);
    35. int opt = 1;
    36. setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 为了确保套接字_listensockfd在关闭后,其地址和端口可以立即被重新使用,而不是等待它们进入TIME_WAIT状态
    37. // bind
    38. struct sockaddr_in local;
    39. memset(&local, 0, sizeof(local));
    40. local.sin_family = AF_INET;
    41. local.sin_port = htons(_port);
    42. local.sin_addr.s_addr = INADDR_ANY;
    43. if (bind(_listensockfd, CONV(&local), sizeof(local)) != 0)
    44. {
    45. lg(Fatal, "bind failed");
    46. exit(BindErr);
    47. }
    48. lg(Info, "bind success, listensockfd: %d", _listensockfd);
    49. // 监听
    50. if (listen(_listensockfd, DefaultBacklog) != 0)
    51. {
    52. lg(Fatal, "listen failed");
    53. exit(ListenErr);
    54. }
    55. lg(Info, "listen success, listensockfd: %d", _listensockfd);
    56. }
    57. void Server(int sockfd)
    58. {
    59. char buffer[4096];
    60. while (true)
    61. {
    62. ssize_t n = read(sockfd, buffer, sizeof(buffer));
    63. if (n > 0)
    64. {
    65. buffer[n] = 0;
    66. std::cout << "client says# " << buffer << std::endl;
    67. std::string message = "server echo# ";
    68. message += buffer;
    69. write(sockfd, message.c_str(), message.size());
    70. }
    71. else if (n == 0)
    72. {
    73. lg(Info, "client quit");
    74. break;
    75. }
    76. else
    77. {
    78. lg(Info, "failed");
    79. break;
    80. }
    81. }
    82. }
    83. void Start()
    84. {
    85. _isrunning = true;
    86. while (_isrunning)
    87. {
    88. struct sockaddr_in t;
    89. socklen_t len = sizeof(t);
    90. int sockfd = accept(_listensockfd, CONV(&t), &len);
    91. if (sockfd < 0)
    92. {
    93. lg(Info, "accept failed");
    94. continue;
    95. }
    96. lg(Info, "accept success, new sockfd:%d", sockfd);
    97. Server(sockfd);
    98. close(sockfd);
    99. }
    100. }
    101. ~TcpServer()
    102. {
    103. }
    104. private:
    105. uint16_t _port;
    106. int _listensockfd;
    107. bool _isrunning;
    108. };
    109. // TcpServer.cpp
    110. #include
    111. #include
    112. #include
    113. #include
    114. #include
    115. #include "Log.hpp"
    116. #include "Comm.hpp"
    117. #include
    118. void Usage(std::string commond)
    119. {
    120. std::cout << commond << " server_ip server_port" << std::endl;
    121. }
    122. bool VisitServer(std::string ip, uint16_t port)
    123. {
    124. // 创建套接字
    125. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    126. if (sockfd < 0)
    127. {
    128. lg(Fatal, "sockfd failed");
    129. return false;
    130. }
    131. // bind -- 交给OS
    132. // connect
    133. struct sockaddr_in server;
    134. memset(&server, 0, sizeof(server));
    135. server.sin_family = AF_INET;
    136. server.sin_port = htons(port);
    137. inet_pton(AF_INET, ip.c_str(), &server.sin_addr);
    138. int n = connect(sockfd, CONV(&server), sizeof(server));
    139. if (n < 0)
    140. {
    141. lg(Fatal, "connect failed");
    142. return false;
    143. }
    144. while (true)
    145. {
    146. std::cout << "Please enter# ";
    147. std::string buffer;
    148. std::getline(std::cin, buffer);
    149. ssize_t n = write(sockfd, buffer.c_str(), buffer.size());
    150. if (n > 0)
    151. {
    152. char buffer[1024];
    153. ssize_t m = read(sockfd, buffer, sizeof(buffer));
    154. if (m > 0)
    155. {
    156. buffer[m] = 0;
    157. std::cout << buffer << std::endl;
    158. }
    159. else if (m == 0)
    160. {
    161. close(sockfd);
    162. return false;
    163. }
    164. else
    165. {
    166. close(sockfd);
    167. return false;
    168. }
    169. }
    170. else
    171. {
    172. break;
    173. }
    174. }
    175. }
    176. int main(int argc, char *args[])
    177. {
    178. if (argc != 3)
    179. {
    180. Usage(args[0]);
    181. exit(1);
    182. }
    183. std::string ip = args[1];
    184. uint16_t port = std::stoi(args[2]);
    185. VisitServer(ip, port);
    186. return 0;
    187. }

    3、客户端代码

    1. // TcpClient.cpp
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include "Log.hpp"
    8. #include "Comm.hpp"
    9. #include
    10. void Usage(std::string commond)
    11. {
    12. std::cout << commond << " server_ip server_port" << std::endl;
    13. }
    14. bool VisitServer(std::string ip, uint16_t port)
    15. {
    16. // 创建套接字
    17. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    18. if (sockfd < 0)
    19. {
    20. lg(Fatal, "sockfd failed");
    21. return false;
    22. }
    23. // bind -- 交给OS
    24. // connect
    25. struct sockaddr_in server;
    26. memset(&server, 0, sizeof(server));
    27. server.sin_family = AF_INET;
    28. server.sin_port = htons(port);
    29. inet_pton(AF_INET, ip.c_str(), &server.sin_addr);
    30. int n = connect(sockfd, CONV(&server), sizeof(server));
    31. if (n < 0)
    32. {
    33. lg(Fatal, "connect failed");
    34. return false;
    35. }
    36. while (true)
    37. {
    38. std::cout << "Please enter# ";
    39. std::string buffer;
    40. std::getline(std::cin, buffer);
    41. ssize_t n = write(sockfd, buffer.c_str(), buffer.size());
    42. if (n > 0)
    43. {
    44. char buffer[1024];
    45. ssize_t m = read(sockfd, buffer, sizeof(buffer));
    46. if (m > 0)
    47. {
    48. buffer[m] = 0;
    49. std::cout << buffer << std::endl;
    50. }
    51. else if (m == 0)
    52. {
    53. close(sockfd);
    54. return false;
    55. }
    56. else
    57. {
    58. close(sockfd);
    59. return false;
    60. }
    61. }
    62. else
    63. {
    64. break;
    65. }
    66. }
    67. }
    68. int main(int argc, char *args[])
    69. {
    70. if (argc != 3)
    71. {
    72. Usage(args[0]);
    73. exit(1);
    74. }
    75. std::string ip = args[1];
    76. uint16_t port = std::stoi(args[2]);
    77. VisitServer(ip, port);
    78. return 0;
    79. }

    我们在熟悉了这些接口之后,还能实现一些其他的有趣的功能,有时间,我会单独出几篇文章来讲述一些基于网络的有趣项目的实现。

    三、总结

    对网络通信的进一步理解

    对于UDP和TCP的接口的介绍暂时就说到这里,下面我们透过上面的编码简单聊聊UDP和TCP

    1、在前面,我们说过网络传输的字节序是大端,而主机分为大端机和小端机,那么为什么我们上面的代码没有对大小端进行相应的处理呢?

    本质是因为我们传输的数据都是char类型的数据,只有1字节,而大小端针对的是多字节数据的存储,如整形,长整型,浮点数等,所以char类型的数据不受大小端影响。故能顺利得出,否则就需要我们手动对数据进行大小端转换的处理。

    2、如何理解面向数据报和面向字节流?

    这里简单说明一下,面向数据报类似我们收取快递,一个箱子就是一个快递,也就是说,数据报是一份一份的,分开的,有边界的。

    而面向字节流类似我们之前在进程通信中讲过的管道,我们可以往里面多次输入数据,我们在读取时可以一次性全拿出来,也能分成多次拿出,取决于我们如何使用这些数据

    从中我们也能看出,tcp协议要比udp协议更难写,相比于收取"快递数据",如何从一堆数据中拼凑出正确的数据显然难度更大(并且具体的收发是由OS实现的,也就是说客户端无法得知我们发送的数据是否全部发送了,服务端也无法得知我们的数据是否完整的到达了)。

    3、我们写的服务端代码难道要像上面这样在前台运行吗,如果用户退出,我们的服务挂掉了怎么办?如何让服务在服务器上一直跑,直到服务器被关掉呢?

    我们写的网络服务,不能在bash中以前台进程的方式运行,真正的服务器,必须在Linux的后台,以守护进程(精灵进程)的方式进行运行。

    什么是守护进程?

    在解释这个之前,我们先来了解一下其他的相关概念

    (父进程和进程大家都很了解了,这里就不做介绍了,这里引入两个新概念---进程组和会话)

    • 进程组是一个或多个进程的集合,每个进程都属于一个进程组。进程组主要目的是为了简化对多个进程的管理。每个进程组都有一个唯一的进程组ID,这个ID通常是进程组中的组长进程的进程ID。组长进程可以创建一个进程组,并在其中创建新的进程。只要进程组中有一个进程存在,进程组就会持续存在,与组长进程是否终止无关。此外,进程组ID在诸如waitpid函数和kill函数的参数中都有使用,使得我们可以对整个进程组内的进程进行统一的管理和操作,比如使用kill命令通过进程组ID来结束整个进程组的进程。
    • 会话则是操作系统中用于管理进程的抽象概念,它提供了一个运行环境和资源共享的上下文。会话由一个或多个进程组组成,每个会话都有一个唯一的会话ID,这个ID与会话的领头进程(即创建会话的进程)的PID相同。会话的主要功能包括进程间通信、控制终端以及作业控制。会话中的进程可以通过进程间通信机制相互通信和共享信息。会话通常与一个控制终端相关联,用于输入和输出的交互。会话首领可以通过控制终端与用户进行交互,并控制终端的行为。此外,会话还提供了作业控制的机制,可以将进程从前台切换到后台,或者从后台切换到前台。

    这里首先强调:不要将这两个概念下的进程间关系往父子上去靠,这是两个不同层面的概念。

    相信大家看完上面这段话还是不太理解这两个概念,下面我们结合bash再来具体的理解一下会话和进程组。

    如何创建守护进程?

    1. 屏蔽信号:根据具体情况,屏蔽一些信号,防止影响守护进程的执行
    2. 创建子进程并退出父进程:因为setsid()需要当前进程不能是进程组的组长,而我们启动的服务器进程就一个,所以它必然是进程组组长,所以我们需要通过fork()函数创建子进程,让子进程来执行setsid()函数
    3. 在子进程中创建新会话:在子进程中,调用setsid()函数来创建一个新的会话,并使子进程成为该会话的会话首领。这样,子进程就完全独立出来,脱离了对控制终端的依赖。
    4. 改变当前工作目录:使用chdir()函数将当前工作目录更改为根目录。当然,也可以根据需要更改到其他路径。
    5. 关闭文件描述符:关闭所有从父进程继承而来的打开文件描述符。这些描述符在守护进程中可能不会被用到,关闭它们可以节省系统资源,并防止可能的文件锁定问题。
    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. const char *root = "/";
    9. void daemon(bool ischdir, bool isclose)
    10. {
    11. // 忽略可能引起程序异常退出的信号
    12. signal(SIGCHLD, SIG_IGN);
    13. signal(SIGPIPE, SIG_IGN);
    14. // 创建子进程 --- 不让自己成为组长
    15. if (fork() > 0) exit(0);
    16. // 设置让自己成为一个新的会话
    17. setsid();
    18. // 改变cwd
    19. if (ischdir)
    20. {
    21. chdir(root);
    22. }
    23. // 关闭文件描述符 / 重定向
    24. if (isclose)
    25. {
    26. close(0);
    27. close(1);
    28. close(2);
    29. }
    30. else
    31. {
    32. int fd = open("/dev/null", O_RDWR); // /dev/null是一个字符设备文件,输入进去的数据被清空,读取出来的数据为空,推荐用这个
    33. dup2(fd, 0);
    34. dup2(fd, 1);
    35. dup2(fd, 2);
    36. close(fd);
    37. }
    38. }
    1. #include "dameon.hpp"
    2. int main()
    3. {
    4. daemon(true,false); // 测试
    5. while(1)
    6. {
    7. sleep(1);
    8. }
    9. return 0;
    10. }

    从上图中,我们得知testd这个进程是孤儿进程,不和任何终端相连,是一个独立的会话, 同时它的当前工作路径被我们改为了/,标准输入/输出/错误也被重定向到了/dev/null这个字符设备文件,要想结束该进程,直接kill -9就行,当然我们也可以通过设置相关的信号用来关闭该守护进程。

    系统其实也帮我们写个daemon这个函数用来创建守护进程

     (具体的函数内部实现和我们写的不太一样,可以看看文档)

    这里推荐自己写,因为不同系统的daeman实现可能不同,不容易把控,还是自己写比较稳妥,可以根据具体需求自己定制设计

    四、附录

    上面客户端和服务端中包含的头文件内容

    1. //InetAddr.hpp
    2. #pragma once
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. class InetAddr // 用来管理网络套接字ip,port
    10. {
    11. public:
    12. InetAddr(const struct sockaddr_in& addr)
    13. {
    14. _ip = inet_ntoa(addr.sin_addr);
    15. _port = ntohs(addr.sin_port);
    16. }
    17. std::string IP()
    18. {
    19. return _ip;
    20. }
    21. uint16_t Port()
    22. {
    23. return _port;
    24. }
    25. std::string DebugPrinit()
    26. {
    27. return _ip + ":" + std::to_string(_port);
    28. }
    29. ~InetAddr()
    30. {}
    31. private:
    32. std::string _ip;
    33. uint16_t _port;
    34. };
    35. // Comm.hpp
    36. enum{
    37. SocketErr=1,
    38. BindErr,
    39. ListenErr
    40. };
    41. #define CONV(addr) ((struct sockaddr *)addr)
    42. // Log.hpp
    43. #pragma once
    44. #include
    45. #include
    46. #include
    47. #include
    48. #include
    49. #include
    50. #include
    51. #include
    52. #include
    53. #include "LockGuard.hpp"
    54. enum
    55. {
    56. Debug,
    57. Info,
    58. Warning,
    59. Error,
    60. Fatal
    61. };
    62. enum
    63. {
    64. Screem = 10,
    65. OneFile,
    66. Files
    67. };
    68. const int defaultstyle = Screem;
    69. const std::string filename = "Log";
    70. const std::string dir = "Log";
    71. std::string LevelToString(int level)
    72. {
    73. switch (level)
    74. {
    75. case Debug:
    76. return "Debug";
    77. case Info:
    78. return "Info";
    79. case Warning:
    80. return "Warning";
    81. case Error:
    82. return "Error";
    83. case Fatal:
    84. return "Fatal";
    85. default:
    86. return "unknown";
    87. }
    88. }
    89. class Log
    90. {
    91. public:
    92. Log():_style(defaultstyle),_filename(filename),_filepath(dir)
    93. {
    94. mkdir(_filepath.c_str(),0775); // 在当前目录下创建目录
    95. pthread_mutex_init(&_mutex,nullptr);
    96. }
    97. std::string local_time()
    98. {
    99. time_t cur = time(nullptr);
    100. struct tm* t = localtime(&cur);
    101. char buffer[128];
    102. // asctime_r(t, buffer);
    103. snprintf(buffer,sizeof(buffer),"%d-%d-%d %d:%d:%d",t->tm_year+1900,t->tm_mon+1,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
    104. return buffer;
    105. }
    106. void Write(const std::string &info, const std::string &suffix)
    107. {
    108. // 可以加锁,保证线程安全 ...
    109. LockGuard lock(&_mutex);
    110. std::string name = _filepath + "/" + _filename + suffix;
    111. std::ofstream ifs(name.c_str(), std::ios_base::out | std::ios_base::app);
    112. if(ifs.is_open())
    113. ifs<
    114. ifs.close();
    115. // std::cout<
    116. }
    117. void WriteToFile(const std::string &info, const std::string& level)
    118. {
    119. switch(_style)
    120. {
    121. case Screem:
    122. std::cout << info;
    123. break;
    124. case OneFile:
    125. Write(info,".all");
    126. break;
    127. case Files:
    128. Write(info,"."+level);
    129. break;
    130. default:
    131. break;
    132. }
    133. }
    134. void Message(int level, const char *format, ...)
    135. {
    136. char buffer[1024];
    137. va_list args;
    138. va_start(args, format);
    139. vsnprintf(buffer, sizeof(buffer), format, args);
    140. va_end(args);
    141. // printf("[%s][%s][%s]\n",LevelToString(level).c_str(),local_time().c_str(),buffer);
    142. char info[4096];
    143. std::string lev = LevelToString(level);
    144. snprintf(info,sizeof(info),"[%s][%s][%s] %s\n",std::to_string(getpid()).c_str(),
    145. lev.c_str(),local_time().c_str(),buffer);
    146. WriteToFile(info,lev);
    147. }
    148. // 将Message函数转换成仿函数,方便调用
    149. void _Message_(int level, const char *format, va_list args)
    150. {
    151. char buffer[1024];
    152. vsnprintf(buffer, sizeof(buffer), format, args);
    153. char info[4096];
    154. std::string lev = LevelToString(level);
    155. snprintf(info,sizeof(info),"[%s][%s][%s] %s\n",std::to_string(getpid()).c_str(),
    156. lev.c_str(),local_time().c_str(),buffer);
    157. WriteToFile(info,lev);
    158. }
    159. void operator()(int level, const char *format, ...)
    160. {
    161. va_list args;
    162. va_start(args, format);
    163. _Message_(level,format,args);
    164. va_end(args);
    165. }
    166. ~Log()
    167. {
    168. pthread_mutex_destroy(&_mutex);
    169. }
    170. // 提供接口,方便我们改变日志的输出
    171. void Enable(int mode)
    172. {
    173. _style = mode;
    174. }
    175. private:
    176. int _style;
    177. const std::string _filename;
    178. const std::string _filepath;
    179. pthread_mutex_t _mutex;
    180. };
    181. Log lg;
    182. class Conf
    183. {
    184. public:
    185. Conf()
    186. {
    187. lg.Enable(Screem);
    188. }
    189. ~Conf()
    190. {}
    191. };
    192. Conf conf;
    193. // nocopy.hpp
    194. #pragma once
    195. class nocopy
    196. {
    197. public:
    198. nocopy()=default;
    199. nocopy(const nocopy&tmp)=delete;
    200. nocopy& operator=(const nocopy&tmp)=delete;
    201. };
  • 相关阅读:
    Redis 中 String 类型的内存开销比较大
    〔002〕Java 基础之语法、数据类型、进制转换、运算符
    【JVM笔记】Java虚拟机栈与常见异常和如何设置栈内存大小
    LLM(大语言模型)常用评测指标之F1-Score
    oracle10数据库迁移
    springboot 项目非docker 部署自动启动
    Zabbix6.2惊喜发布!特别优化中大型环境部署的性能!
    各个数据库存二进制大文件的性能测试
    使用机器学习协助灾后救援
    git实战命令(小技巧篇hp)
  • 原文地址:https://blog.csdn.net/V_zjs/article/details/137426801