赞
踩
目录
理解源IP地址和目的IP地址可以通过类比日常通信的方式来解释。想象一下寄信的场景,这里有两个参与者:寄件人(源)和收件人(目的)。
源IP地址: 寄件人的地址就好比网络通信中的源IP地址。这个地址标识了数据包的出发地,即发送数据包的设备。就像在寄信时写明寄件人地址,网络数据包中的源IP地址用于标识数据的来源。
目的IP地址: 收件人的地址就好比网络通信中的目的IP地址。这个地址标识了数据包的目的地,即接收数据包的设备。就像在寄信时写明收件人地址,网络数据包中的目的IP地址用于指示数据的最终目标。
通信过程的类比:
综上所述,端口号在网络通信中扮演着重要的角色,通过它们,不同的应用程序能够在同一主机上并发运行,并通过网络进行通信。
源端口号和目的端口号是在网络通信中用于标识发送和接收进程的端口。这是在传输层(通常是TCP或UDP协议)中使用的概念。
为什么需要端口号? 在一个主机上可能同时运行多个应用程序,每个应用程序都需要独立地发送和接收数据。端口号使得数据包能够被正确地路由到相应的应用程序,实现多个应用程序之间的并发通信。
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的网络传输协议,位于OSI模型的传输层。TCP确保数据的可靠性和顺序传输,它通过将数据划分为小的数据段,并使用序号和确认号来跟踪数据的传输和接收。
以下是TCP协议的一些关键特点:
TCP广泛用于各种应用,特别是对于需要可靠数据传输和顺序传输的应用,如网页浏览、文件传输、电子邮件等
UDP(User Datagram Protocol)是一种无连接的、简单的面向数据报的传输协议,位于OSI模型的传输层。相对于TCP,UDP更注重传输效率而不是可靠性和顺序传输。
以下是UDP协议的一些关键特点:
总体而言,UDP和TCP是两种不同的传输协议,各自适用于不同的应用场景。UDP强调快速传输和实时性,而牺牲了可靠性和顺序性;而TCP则更注重数据传输的可靠性、顺序性和错误处理。选择使用UDP还是TCP取决于具体的应用需求。
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节数。
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
总体而言,网络字节序的规范是为了在不同体系结构的计算机之间实现可靠的数据通信,而TCP/IP协议族对网络字节序采用大端字节序的规定是为了提供一致性和互操作性。
什么是主机字节序?什么是网络字节序?
主机字节序和网络字节序是两种不同的字节序(即字节的存储顺序)规定,用于确保在不同计算机体系结构之间进行数据交换时的一致性。
在进行网络通信时,如果发送方和接收方的主机字节序不同,就需要进行字节序的转换,以确保数据在传输过程中被正确解释。这种转换通常是由网络库或操作系统提供的函数完成的。所以主机字节序指的是自己电脑对数据存储的方式,网络字节序指的是对接受或者发送的数据的存储方式。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
这些函数名很好记,h开头的表示host(主机),n开头的表示network(网络),l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回 ;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
- // 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
- int socket(int domain, int type, int protocol);
- // 绑定端口号 (TCP/UDP, 服务器)
- int bind(int socket, const struct sockaddr *address,
- socklen_t address_len);
- // 开始监听socket (TCP, 服务器)
- int listen(int socket, int backlog);
- // 接收请求 (TCP, 服务器)
- int accept(int socket, struct sockaddr* address,
- socklen_t* address_len);
- // 建立连接 (TCP, 客户端)
- int connect(int sockfd, const struct sockaddr *addr,
- socklen_t addrlen);
在"socket API"中,"API"是指"Application Programming Interface",即应用程序编程接口。它表示一组定义了函数、协议、数据结构和常量等编程接口的规范,用于在应用程序和操作系统之间进行通信和交互。
Socket API 是一种提供套接字编程接口的 API,这些接口定义了在程序中如何创建、配置、连接、发送和接收数据等网络操作。Socket API允许开发者使用编程语言中的函数、类或方法来执行与网络相关的任务,而无需深入了解底层网络协议的细节。
Socket API 是一个抽象的网络编程接口,允许开发人员使用统一的编程接口来处理各种网络通信,包括 IPv4、IPv6 以及 Unix 域套接字(UNIX Domain Socket)等。
不同的网络协议有着各自不同的地址格式。这些地址格式对应着不同的套接字地址结构体,例如:
这些不同的地址结构体都允许在程序中使用统一的 sockaddr 结构体进行通用性的传递(也就是说sockaddr_in 和sockaddr_un 都可以通过 sockaddr 结构体和类型转换,用来传递给sockaddr)。sockaddr 结构体是一个通用的套接字地址结构体,用于在各种情况下传递套接字地址信息。为了实现这种通用性,Socket API 提供了类型转换功能,允许开发者在不同的地址结构体之间进行转换。
这样,通过 sockaddr 结构体和类型转换,开发人员可以编写更加通用、可移植的网络应用程序,而无需在不同的网络协议之间切换时修改大量的代码。Socket API 提供了一种抽象层,使得程序员能够更加方便地处理不同协议的网络通信,提高了代码的可移植性和复用性。
注:sockaddr_in 和 sockaddr_un 是在网络编程中常用的两个结构体,用于表示套接字地址。它们分别用于处理基于网络通信的套接字(如IPv4套接字)和基于本地通信的套接字(Unix域套接字)。
sockaddr_in 结构体主要设计用于表示 IPv4 地址信息,因此在通常情况下,它主要用于网络通信而不是本地通信。然而,如果你愿意,你可以在本地环境中使用 sockaddr_in 进行通信,但这并不是通常的做法。
在 C 语言中,struct sockaddr 是一个通用的套接字地址结构体,它被用来表示各种类型的套接字地址,包括 IPv4、IPv6 以及 Unix 域套接字。这个结构体的定义如下:
- struct sockaddr {
- unsigned short sa_family; // address family, AF_xxx
- char sa_data[14]; // 14 bytes of protocol address
- };
其中,sa_family 字段表示地址的类型,可以是 AF_INET 表示 IPv4,AF_INET6 表示 IPv6,而对于 Unix 域套接字,则会使用不同的地址家族。sa_data 则是用来存储具体的地址信息。
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16
位端口号和32位IP地址。
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,
不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好
处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为
参数;
例如,如果你有一个 struct sockaddr * 指针,你可以这样检查地址类型并转换为对应的结构体:
- struct sockaddr *addr; // 指向套接字地址的指针
-
- // 检查地址类型
- if (addr->sa_family == AF_INET) {
- // IPv4 地址
- struct sockaddr_in *ipv4_addr = (struct sockaddr_in *)addr;
- // 在这里可以使用 ipv4_addr 来访问 IPv4 地址的具体信息
- } else if (addr->sa_family == AF_INET6) {
- // IPv6 地址
- struct sockaddr_in6 *ipv6_addr = (struct sockaddr_in6 *)addr;
- // 在这里可以使用 ipv6_addr 来访问 IPv6 地址的具体信息
- } else {
- // 其他类型的地址处理
- }
这种通用性使得你的程序更加灵活,可以适应不同类型的网络地址而无需修改大量代码。这也是 Socket API 设计的一个重要特点,提高了代码的可移植性和通用性。
以下是 sockaddr_in 的原型定义:
- struct sockaddr_in {
- sa_family_t sin_family; // 地址族,一般为 AF_INET
- in_port_t sin_port; // 端口号,使用网络字节序表示
- struct in_addr sin_addr; // IPv4 地址
- char sin_zero[8]; // 填充字段,通常设置为 0
- };
在该结构体中,包含了以下几个成员:
需要注意的是,sockaddr_in 结构体是用于描述 IPv4 地址和端口号的,而不是实际的 IP 地址和端口号。
例:
因为使用的 bzero 进行了初始化为0,所以sin_zero一并被设置为0了。
注:sockaddr_in 是 Socket(套接字) Address(地址) INternet(互联网) 的缩写。
sockaddr_un是用于表示UNIX域套接字地址的结构体。以下是sockaddr_un的原型定义:
- #include <sys/un.h>
-
- struct sockaddr_un {
- sa_family_t sun_family; // 地址族,一般为 AF_UNIX
- char sun_path[108]; // 路径名
- };
在该结构体中,包含了以下两个成员:
需要注意的是,UNIX域套接字不需要端口号,而是使用文件系统中的路径名来进行通信。因此,在使用sockaddr_un结构体表示UNIX域套接字地址时,需要指定sun_family为AF_UNIX,并将路径名存放在sun_path成员中。
socket() 函数是一个系统调用,在Socket编程中用于创建新的套接字的函数。这个函数通常由操作系统提供,并且在不同的编程语言中可能有一些细微的差异。以下是通常情况下,在类Unix系统上(包括Linux和macOS)使用C语言进行Socket编程时的基本形式:
- #include <sys/types.h>
- #include <sys/socket.h>
-
- int socket(int domain, int type, int protocol);
其中:
例如,在创建一个基于IPv4和UDP的套接字时,可以使用以下代码:
- #include <sys/types.h>
- #include <sys/socket.h>
-
- int main() {
- int sockfd;
- sockfd = socket(AF_INET, SOCK_DGRAM, 0);
- if (sockfd == -1) {
- // 处理套接字创建失败的情况
- }
-
- // 其他操作,如绑定、监听、连接等
-
- return 0;
- }
需要注意的是,socket() 函数只是创建了一个套接字,还需要后续的操作(如绑定、监听、连接等)才能进行实际的网络通信。这个函数返回一个整数类型的套接字 文件描述符,用于后续对该套接字的引用。如果发生错误,它返回 -1,此时应该根据具体情况处理错误。
注:尽管套接字在某种程度上它的本质被视为文件,但它们与普通文件还是有很大的区别。普通文件通常存储在磁盘上,但套接字提供了一种用于网络通信的机制。套接字提供了客户端和服务器之间的连接点,使得数据可以通过网络传输到另一个套接字,而不是被存储在磁盘上。
bind函数是一个系统调用,在网络编程中,bind() 函数用于将一个套接字(Socket)绑定到一个特定的地址和端口上。它是 Socket API 提供的函数之一,通过它可以设置套接字的本地地址和端口号,从而使套接字能够与特定的网络地址进行关联。
注:在网络编程中,"网络地址"指的是用于标识网络上主机(计算机)或者网络设备的唯一标识。网络地址由 IP 地址和端口号组成。
IP 地址是一个由数字和点分隔符组成的标识符,用于标识网络上的主机。IP 地址分为 IPv4 地址(例如:"192.0.2.1")。IP 地址用于在网络中唯一标识一个主机,类似于现实生活中的街道地址。
换句话说,网络地址是用于标识网络上的主机或设备的唯一标识,它由 IP 地址和端口号组成。通过使用 bind() 函数,我们可以将套接字与指定的网络地址关联起来,使得套接字可以与特定的主机(通过 IP 地址)和应用程序(通过端口号)进行通信。
bind() 函数的原型如下所示:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
bind() 函数在成功时返回 0,它主要完成以下两个任务:
下面是 bind() 函数的返回值含义:
当 bind() 函数失败时,常见的错误码可能包括以下一些:
在调用 bind() 函数之前,通常需要初始化地址结构体,并将要绑定的本地地址和端口信息填充到结构体中。然后,通过 bind() 函数将套接字与该地址进行绑定。
示例代码(C语言):
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
-
- int main() {
- // 创建套接字
- int sockfd = socket(AF_INET, SOCK_STREAM, 0);
-
- // 初始化服务器地址
- struct sockaddr_in server_addr;
- server_addr.sin_family = AF_INET;
- server_addr.sin_port = htons(1234);
- server_addr.sin_addr.s_addr = INADDR_ANY;
-
- // 绑定套接字与地址
- int bind_result = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
- if(bind_result < 0) {
- perror("bind failed");
- return -1;
- }
-
- // ...
- // 其他处理逻辑
- // ...
-
- return 0;
- }

上述示例代码中,通过 socket() 创建了一个套接字,然后初始化了一个 sockaddr_in 结构体描述了服务器的地址信息,最后通过 bind() 函数将套接字与该地址绑定在一起。
这样,服务器套接字就绑定了指定的本地地址和端口,它可以使用这个地址和端口在网络上监听并接收客户端的连接请求。
htons() 函数是一个用于主机字节序(host byte order)和网络字节序(network byte order)之间进行转换的函数,其中网络字节序是大端字节序(big-endian)。
在计算机网络编程中,经常需要将数据从主机字节序转换为网络字节序,或者反之,以便正确地在网络上传输数据。这种字节序转换是因为不同的计算机体系结构使用不同的字节序,而网络协议通常要求使用一种特定的字节序。
函数原型如下:
- #include <arpa/inet.h>
-
- uint16_t htons(uint16_t hostshort);
返回值:返回网络字节序的16位整数。
示例:
- #include <stdio.h>
- #include <arpa/inet.h>
-
- int main() {
- uint16_t host_short = 0x1234; // 4660 in decimal
-
- // Convert from host to network byte order
- uint16_t net_short = htons(host_short);
-
- printf("Host short: 0x%x\n", host_short);
- printf("Network short: 0x%x\n", net_short);
-
- return 0;
- }
在这个例子中,htons 函数将主机字节序的 16 位整数转换为网络字节序,然后打印出两者的值。需要注意的是,这样的字节序转换通常在网络编程中处理套接字时用到,以确保正确的数据传输。
这里只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换。
注:简单来说地址转换函数指的是把字符串风格的IP地址转换成4字节整形的IP地址 或者 把4字节整形的IP地址转换成字符串风格的IP地址
字符串转in_addr类型的函数:
in_addr类型转字符串的函数:
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。
代码示例:
关于inet_ntoa
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放,那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
运行结果如下:
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。
思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢? 在APUE中, 明确提出inet_ntoa不是线程安全的函数; 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁; 可以自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题; 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。
inet_ntoa 函数在早期的实现中通常不是线程安全的,因为它使用了一个静态缓冲区来保存转换后的字符串,而该缓冲区是被所有调用线程共享的。因此,在多线程环境下,多个线程同时调用 inet_ntoa 可能会导致数据混乱或覆盖,因为它们会共享相同的静态缓冲区。
在实际使用中,一些系统的库实现可能对 inet_ntoa 进行了改进,引入了一些线程安全的机制,例如使用互斥锁(mutex)来保护静态缓冲区。因此,你在某些系统上的测试中可能没有观察到问题。
然而,这种依赖于实现的行为是不可靠的,因为不同的系统和库版本可能有不同的实现方式。为了确保在多线程环境下安全使用IP地址转换,推荐使用更现代的函数 inet_ntop,该函数不使用静态缓冲区,而是由调用者提供一个缓冲区来保存结果,从而避免了线程安全问题。这种做法更加可靠,并且符合现代编程的最佳实践。
示例使用 inet_ntop 的代码如下:
- #include <stdio.h>
- #include <arpa/inet.h>
-
- int main() {
- struct in_addr addr;
- inet_aton("192.168.1.1", &addr);
-
- char buffer[INET_ADDRSTRLEN]; // Assuming INET_ADDRSTRLEN is large enough
- const char *result = inet_ntop(AF_INET, &addr, buffer, INET_ADDRSTRLEN);
-
- if (result != NULL) {
- printf("IP address: %s\n", buffer);
- } else {
- perror("inet_ntop");
- }
-
- return 0;
- }

这里使用了 inet_ntop 函数,它更安全,因为它不使用静态缓冲区,而是使用由调用者提供的缓冲区。
recvfrom()函数通常用于UDP套接字。recvfrom() 函数是一个系统调用,用于从指定的套接字 sockfd 接收数据。它的函数原型如下:
- ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
- struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
返回值:
注意事项:
请注意,以上是基于 C 语言的函数原型和参数说明,具体的使用方式还需要根据具体的编程语言和套接字库来确定。
使用 recvfrom() 的一个简单例子如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <arpa/inet.h>
-
- int main() {
- int sockfd;
- struct sockaddr_in server_addr, client_addr;
- socklen_t client_len = sizeof(client_addr);
- char buffer[1024];
-
- // 创建UDP套接字
- if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
- perror("socket");
- exit(EXIT_FAILURE);
- }
-
- // 设置服务器地址
- memset(&server_addr, 0, sizeof(server_addr));
- server_addr.sin_family = AF_INET;
- server_addr.sin_addr.s_addr = INADDR_ANY;
- server_addr.sin_port = htons(8080);
-
- // 绑定套接字到地址和端口
- if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
- perror("bind");
- exit(EXIT_FAILURE);
- }
-
- // 接收数据
- ssize_t recv_bytes = recvfrom(sockfd, buffer, sizeof(buffer), 0,
- (struct sockaddr *)&client_addr, &client_len);
- if (recv_bytes == -1) {
- perror("recvfrom");
- exit(EXIT_FAILURE);
- }
-
- // 打印接收到的数据
- printf("Received data from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
- printf("Data: %.*s\n", (int)recv_bytes, buffer);
-
- // 关闭套接字
- close(sockfd);
-
- return 0;
- }

这个例子创建了一个UDP服务器,绑定到本地地址和端口,然后使用 recvfrom() 接收从客户端发送过来的数据。
sendto() 函数是用于通过一个已连接或未连接的套接字发送数据的函数,通常用于UDP(无连接)套接字。
sendto() 函数常用于UDP套接字,而UDP是无连接的协议。所谓“无连接”是指在发送数据之前不需要先建立连接。相比之下,TCP是一种有连接的协议,它要求在数据传输之前先建立一个连接。
在使用UDP时,通信的两端并不需要在彼此之间建立持久的连接。相反,UDP是面向数据报的,每个数据包都是一个独立的实体,不依赖于之前或之后的数据包。因此,sendto() 函数用于发送UDP数据包,而不需要提前建立连接。
总的来说,UDP是一种无连接的协议,因此在使用UDP套接字时,发送数据的过程中不需要事先建立连接。
其原型为:
- ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
- const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明:
该函数返回已发送的字节数,如果出现错误则返回 -1。
注意事项:
请注意,以上是基于 C 语言的函数原型和参数说明,具体的使用方式还需要根据具体的编程语言和套接字库来确定。
使用 sendto() 的一个简单例子如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <arpa/inet.h>
-
- int main() {
- int sockfd;
- struct sockaddr_in server_addr;
- char buffer[] = "Hello, UDP Server!";
-
- // 创建UDP套接字
- if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
- perror("socket");
- exit(EXIT_FAILURE);
- }
-
- // 设置服务器地址
- memset(&server_addr, 0, sizeof(server_addr));
- server_addr.sin_family = AF_INET;
- server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
- server_addr.sin_port = htons(8080);
-
- // 发送数据
- ssize_t send_bytes = sendto(sockfd, buffer, sizeof(buffer), 0,
- (struct sockaddr *)&server_addr, sizeof(server_addr));
- if (send_bytes == -1) {
- perror("sendto");
- exit(EXIT_FAILURE);
- }
-
- printf("Sent %d bytes to the server.\n", (int)send_bytes);
-
- // 关闭套接字
- close(sockfd);
-
- return 0;
- }

这个例子创建了一个UDP客户端,通过 sendto() 函数向指定的服务器地址发送数据。
popen 函数是一个在Linux和Unix系统中提供的标准C库函数,用于创建一个管道并启动一个子进程。该函数提供了一个方便的接口,允许从父进程向子进程发送数据或从子进程接收数据。
函数原型如下:
FILE *popen(const char *command, const char *mode);
popen 函数返回一个文件指针,可以用于读取或写入与子进程的标准输入或输出相关联的管道。
使用示例:
- #include <stdio.h>
-
- int main() {
- FILE *fp;
- char buffer[1024];
-
- // 执行一个命令并从子进程读取输出
- fp = popen("ls -l", "r");
- if (fp == NULL) {
- perror("popen");
- return 1;
- }
-
- // 读取子进程的输出
- while (fgets(buffer, sizeof(buffer), fp) != NULL) {
- printf("%s", buffer);
- }
-
- // 关闭文件指针
- pclose(fp);
-
- return 0;
- }

在上面的示例中,popen 执行了一个 ls -l 命令,并通过管道读取了子进程的输出。然后,父进程使用 fgets 函数从管道中读取输出,并将其打印到标准输出。最后,使用 pclose 关闭文件指针,等待子进程的结束。
需要注意的是,popen 函数在使用时应当小心防范潜在的安全风险,避免因为用户提供的命令而导致安全漏洞。例如,应该避免直接将用户输入的字符串作为 command 参数传递给 popen,以防止命令注入攻击。
- //udp_client.cpp
- #pragma once
-
- #include <iostream>
- #include <cstring>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <pthread.h>
- #include "err.hpp"
- //#include "udp_client.hpp"//因为把功能写在了一起,所以没有.hpp文件
- using namespace std;
-
- //127.0.0.1:表示本地环回,就表示的就是当前主机,通常用来进行本地通信或者测试,它就是你自己主机默认的IP地址,如何一台机器都有
- //它就是用来进行走我们的网络协议栈,但不是把数据发送到网络,只是在网络当中把我的数据转发一下,也就是转发给自己,它实际上就是在
- //与自己通信,通常是用来在本地进行测试我们写的客户端和服务器
- //启动服务器和客户端,在服务器输入 ./udp_server [port] 进行启动,在客户端输入 ./client 127.0.0.1 [port]进行启动,即可开始测试
- //如果想让其他人也给你的服务器进行通信,只需要把 client 发送给别人,让他启动后 把 127.0.0.1 改为你主机的公网IP即可连接上你的服务器进行通信,前提是你的服务器要先启动。
- //这就是为什么我们在写服务器的时候 local.sin_addr.s_addr = INADDR_ANY;//让我们的udp_server在启动的时候,自动bind本主机上的任意IP的原因,服务器将监听所有可用的
- //网络接口上的任意lP地址。当将服务器绑定到INADDR_ANY时,它将侦听来自本地主机上所有网络接口的数据
- static void usage(std::string proc)//使用手册
- {
- cout << "Usage:\n\t" << proc << " serverip serverport\n" << endl;
- }
-
- void *recver(void *args)
- {
- //接受
- int sock = *(static_cast<int *>(args));
-
- while (true)
- {
- // 接受
- char buffer[2048];
- struct sockaddr_in temp;
- socklen_t len = sizeof(temp);
- int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
- if (n > 0)
- {
- buffer[n] = 0;
- cout << buffer << endl; //1
- }
- }
- }
-
- int main(int argc, char* argv[])
- {
- if (argc != 3)
- {
- usage(argv[0]);
- exit(USAGE_ERR);
- }
- string serverip = argv[1];
- uint16_t serverport = atoi(argv[2]);
-
- //1.创建socket(套接字)接口,本质就是打开网络文件
- int sock = socket(AF_INET, SOCK_DGRAM, 0);//#define AF_INET PF_INET,所以这里使用PF_INET和AF_INET是一样的
- if(sock < 0)
- {
- cout << "create socket error: " << strerror(errno) << endl;
- exit(SOCKET_ERR);
- }
- //cout << "create socket success: " << sock << endl;//输出文件描述符
-
- //client这里要不bind?要的,socket通信的本质:[clientIP : clientPort, serverIP : serverPort] 来进行标识双方的唯一性之后,进行网络版本的进程间通信
- //那么要不要自己bing?不需要,也不要自己bind,由OS自动给我们进行bing。-- 为什么呢?client的port要让OS随机分配,防止client出现启动冲突,
- //在我们的系统中有许多不同的进程,为了防止不同的应用进程使用同一个端口号,所以要让OS随机分配,防止client出现启动冲突。
- //server 为什么要自己bing?1.众所周知server的端口不能随意改变。2.同一家公司的port号需要统一规范化
- //什么时候自动bing的?
-
- // 明确server是谁
- struct sockaddr_in server;
- memset(&server, 0, sizeof(server));
- server.sin_family = AF_INET;
- server.sin_port = htons(serverport);
- server.sin_addr.s_addr = inet_addr(serverip.c_str());//为什么服务器不需要指定IP地址,而客户端需要?
- //在服务器端,将套接字的 local.sin_addr.s_addr 设置为 INADDR_ANY 可以使服务器监听在本机的所有可用 IP 地址上。这样服务器就能够接收到发送到任何一个 IP 地址的连接请求。
- //然而,在客户端连接到服务器时,需要明确指定要连接的目标服务器的 IP 地址。这是因为客户端需要知道目标服务器的 IP 地址才能建立与服务器的连接。
- //客户端通过将 server.sin_addr.s_addr 设置为目标服务器的 IP 地址来指定要连接的服务器。这样客户端就能够将连接请求发送到目标服务器指定的 IP 地址上,与服务器建立连接。
- //总结起来,服务器端通过将套接字绑定到 INADDR_ANY 来监听本机的任意 IP 地址上的连接请求。而客户端需要明确指定要连接的目标服务器的 IP 地址。这样服务器端和客户端才能正确地建立连接并进行通信。
-
- pthread_t tid;
- pthread_create(&tid, nullptr, recver, &sock);
-
- while(true)
- {
- // 用户输入
- string message;
- cerr << "please Enter Your Message# ";//2
- //cin >> message;
- getline(cin, message);
-
- //什么时候自动bing的?在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP。1.bind 2.构建发送的数据报文
- //发送
- sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
- }
-
- return 0;
- }

- //udp_server.cpp
- #include "udp_server.hpp"
- #include <memory>
- #include <cstdio>
-
- using namespace ns_server;
-
- static void usage(string proc)//使用手册
- {
- cout << "Usage:\n\t" << proc << " port\n" << endl;
- }
-
- // 上层的业务处理,不关心网络发送,只负责信息处理即可
- std::string transactionString(std::string request)
- {
- std::string result;
- char c;
- for (auto &r : request)
- {
- if (islower(r))
- {
- c = toupper(r);//用于将字符转换为大写形式
- result.push_back(c);
- }
- else
- {
- result.push_back(r);
- }
- }
-
- return result;
- }
- static bool isPass(const string &command)
- {
- bool pass = true;
- auto pos = command.find("rm");
- if(pos != string::npos) pass=false;
- pos = command.find("mv");
- if(pos != string::npos) pass=false;
- pos = command.find("while");
- if(pos != string::npos) pass=false;
- pos = command.find("kill");
- if(pos != string::npos) pass=false;
- return pass;
- }
-
- // 在你的本地把命令给我,server再把结果给你!
- // ls -a -l
- std::string excuteCommand(string command) // command就是一个命名
- {
- // 1. 安全检查
- if(!isPass(command)) return "you are bad man!";
-
- // 2. 业务逻辑处理
- FILE *fp = popen(command.c_str(), "r");
- if(fp == nullptr) return "None";
- // 3. 获取结果了
- char line[1024];
- std::string result;
- while(fgets(line, sizeof(line), fp) != NULL)
- {
- result += line;
- }
- pclose(fp);
-
- return result;
- }
-
- int main(int argc, char* argv[])
- {
- if (argc != 2)
- {
- usage(argv[0]);
- exit(USAGE_ERR);
- }
- uint16_t port = atoi(argv[1]);
-
- //unique_ptr<UdpServer> usvr(new UdpServer("1.1.1.1", 8082));//服务器不能指定需要bing的IP地址
- //unique_ptr<UdpServer> usvr(new UdpServer(transactionString, port));
- //unique_ptr<UdpServer> usvr(new UdpServer(excuteCommand, port));
- unique_ptr<UdpServer> usvr(new UdpServer(port));
-
- // usvr->InitServer();//服务器的初始化
- usvr->start();
-
- return 0;
- }

- //udp_server.hpp
- #pragma once
-
- #include <iostream>
- #include <cerrno>
- #include <cstring>
- #include <cstdlib>
- #include <functional>
- #include <strings.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <pthread.h>
- #include <arpa/inet.h>
- #include <unordered_map>
- #include "err.hpp"
- #include "RingQueue.hpp"
- #include "lockGuard.hpp"
- #include "Thread.hpp"
-
- using namespace std;
-
- namespace ns_server
- {
- const static uint16_t default_port = 8080;//内置端口号
- using func_t = function<string (string)>;//定义一个函数,返回值和参数都是string
- class UdpServer
- {
- public:
- UdpServer(uint16_t port = default_port): port_(port)
- {
- std::cout << "server addr: " << port_ << std::endl;
- pthread_mutex_init(&lock, nullptr);
-
- p = new Thread(1, bind(&UdpServer::Recv, this));
- c = new Thread(1, bind(&UdpServer::Broadcast, this));
- }
-
- void start()
- {
- //1.创建socket(套接字)接口,本质就是打开网络文件,也就是一个文件描述符(套接字)
- sock_ = socket(AF_INET, SOCK_DGRAM, 0);//#define AF_INET PF_INET,所以这里使用PF_INET和AF_INET是一样的,SOCK_DGRAM:创建数据报套接字
- if(sock_ < 0)
- {
- cout << "create socket error: " << strerror(errno) << endl;
- exit(SOCKET_ERR);
- }
- cout << "create socket success: " << sock_ << endl;//输出文件描述符 -- 3,因为0,1,2被占用
-
- //2.因为要给服务器指明IP地址和Port(端口号),所以要先定义一个sockaddr_in类型(套接字段) -- 这也是属于bing操作的一部分
- struct sockaddr_in local;//定义一个sockadd_in套接字结构体类型用于指定(存储)服务器的IP和端口号,这个local在定义哪里?用户空间的特定函数的栈帧上,不在内核中,所以下面的操作只是在对local进行初始化
- //初始化local
- bzero(&local, sizeof(local));//bzero和memset功能是差不多的,都用于将一段内存区域清零
- local.sin_family = AF_INET;//AF_INET:IPV4地址族,表示IP地址的类型
- local.sin_port = htons(port_);//设置端口号:如果我给你发消息你想给我发回来,你就需要知道我的IP地址和端口号,换句话说端口号这个两字节的数据必须出现在网络当中
- //所以port这个字数据是要被发到网络当中的,但是你在类内定义的port叫做本地主机序列构建的port,你需要把这个port从主机序列转换成网络序列。
- //iner_addr:
- //1.字符串风格的IP地址,转换成为4字节int,"1.1.1.1" -> uint32_t
- //2.需要将主机序列转换成为网络序列 -- iner_addr函数替我们完成了这两件事
- //local.sin_addr.s_addr = inet_addr(ip_.c_str());
- //3.云服务器,或者一款服务器,一般不要指明某一个确定的IP
- local.sin_addr.s_addr = INADDR_ANY;//让我们的udp_server在启动的时候,自动分配bind本主机上的任意IP
- //虽然可以写成local.sin_addr这是C++支持的,但是sockadd_in是C语言的东西,只能在定义时整体赋值初始化,不允许第二次整体赋值,因为local.sin_addr
- //也是一个结构体,所以建议写成这种形式:local.sin_addr.s_addr
- //INADDR_ANY 本质是一个全0的整数,表示接受任意一个IP,因为是全0要不要使用htons进行主机序列转换成网络序列都无所谓。
-
- //3.bind(绑定)套接字,把定义的local绑定到 sock_ 里
- int n = bind(sock_, (struct sockaddr*)&local, sizeof(local));
- if(n < 0)
- {
- cerr << "bing socket error: " << strerror(errno) << endl;
- exit(BIND_ERR);
- }
- cout << "bing socket success: " << sock_ << endl;
-
- p->run();
- c->run();
- }
-
- void addUser(const std::string &name, const struct sockaddr_in &peer)
- {
- // onlineuserp[name] = peer;
- LockGuard lockguard(&lock);
- auto iter = onlineuser.find(name);
- if (iter != onlineuser.end())
- return;
- // onlineuser.insert(std::make_pair<const std::string, const struct sockaddr_in>(name, peer));
- onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name, peer));
- }
-
- //注:为什么收和发信息时不需要从主机序列转换成网络序列?因为 recvfrom 和 sendto 会自动帮我们在底层做大小端的转化,以及后面的TCP的读写方法(收和发),也是自动帮我们做大小端的转化
- //只有在bind阶段,也就是启动服务器阶段,有些属性是需要我们去把它进行大小端的转化的。如上面的:struct sockaddr_in local 的属性就是如此,而下面的 struct sockaddr_in peer
- //和 user 就会自动进行大小端的转换
- void Recv()
- {
- char buffer[1024];//接收/发送 存储数据的缓冲区
- while(true)//服务器本质就是死循环
- {
- // 收
- struct sockaddr_in peer;//定义一个用于存储客户端的 IP 地址和端口号的套接字结构体,所以不需要和上面一样进行初始化,因为是用来接收的。
- socklen_t len = sizeof(peer); // 这里一定要写清楚,未来你传入的缓冲区大小,也就是sockaddr_in结构体的大小
- int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);//sizeof(buffer) - 1保留一个位置添加 \0
- if (n > 0)
- buffer[n] = '\0';
- else
- continue;
-
- // 提取client信息 -- debug
- string clientip = inet_ntoa(peer.sin_addr);
- uint16_t clientport = ntohs(peer.sin_port);
- cout << clientip << "-" << clientport << "# " << buffer << endl;
-
- // 构建一个用户,并检查
- std::string name = clientip;
- name += "-";
- name += std::to_string(clientport);
- // 如果不存在,就插入,如果存在,什么都不做
- addUser(name, peer);
- rq.push(buffer);
-
- // // 做业务处理
- // std::string message = service_(buffer);
-
- // 发 -- 把接受到的数据经过业务处理之后发回给客户端
- // sendto(sock_, message.c_str(), message.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
- // sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, sizeof(peer));
- }
- }
-
- //发
- void Broadcast()
- {
- while (true)
- {
- string sendstring;
- rq.pop(&sendstring);
-
- vector<struct sockaddr_in> v;
- {
- LockGuard lockguard(&lock);
- for (auto user : onlineuser)
- {
- v.push_back(user.second);
- }
- }
- for (auto user : v)
- {
- // cout << "Broadcast message to " << user.first << sendstring << endl;
- sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr *)&(user), sizeof(user));
- cout << "send done ..." << sendstring << endl;
- }
- }
- }
-
- ~UdpServer()
- {
- pthread_mutex_destroy(&lock);
- c->join();
- p->join();
-
- delete c;
- delete p;
- }
- private:
- int sock_;
- uint16_t port_;//端口号
- //func_t service_;//测试完解决网络的IO问题之后,开始进行业务处理 -- 回调函数
- unordered_map<string, struct sockaddr_in> onlineuser;//在线用户
- pthread_mutex_t lock;
- RingQueue<string> rq;
- Thread* c;
- Thread* p;
- //string ip_;//后面要专门研究一下,因为后面要去掉这个IP
- };
- }

- //Makefile
- .PHONY:all
- all: udp_client udp_server
-
- udp_client:udp_client.cc
- g++ -o $@ $^ -std=c++11 -lpthread
-
- udp_server:udp_server.cc
- g++ -o $@ $^ -std=c++11 -lpthread
-
- .PHONY:clean
- clean:
- rm -f udp_client udp_server
- //RingQueue.hpp
- #pragma once
-
- #include <iostream>
- #include <vector>
- #include <semaphore.h>
- #include <pthread.h>
-
- using namespace std;
-
- static const int N = 5;
-
- template<class T>
- class RingQueue
- {
- private:
- void P(sem_t &s)
- {
- sem_wait(&s);//P操作 --
- }
-
- void V(sem_t &s)
- {
- sem_post(&s);//V操作 ++
- }
-
- void Lock(pthread_mutex_t &m)
- {
- pthread_mutex_lock(&m);
- }
-
- void UnlocK(pthread_mutex_t &m)
- {
- pthread_mutex_unlock(&m);
- }
-
- public:
- RingQueue(int num = N) :_ring(num), _cap(num)
- {
- sem_init(&_data_sem, 0, 0);
- sem_init(&_space_sem, 0, num);
- _c_step = _p_step = 0;
-
- pthread_mutex_init(&_c_mutex, nullptr);
- pthread_mutex_init(&_p_mutex, nullptr);
- }
-
- void push(const T &in)//生产
- {
- //先申请锁,还是先申请信号量?答案是推荐先申请信号量
- //如果我们先申请锁,那么也就意味着生产线程只要持有锁了,其他线程就没有机会进入后续的代码逻辑了
- //更加意味着其他线程在当前线程持有锁期间,其他线程也就只能在外部等待,但是如果先申请信号量,我们
- //就可以先把资源分配好,就好比一间教室你是让一个一个的人进来找好坐位,还是直接让一批人进来找好坐位,那个
- //速度更快?当然是让一批人进来找坐位速度更快,效率更好,所以下面的消费者也是如此。
- //所以先申请信号量,在进行加锁。
- P(_space_sem);//P操作 --
- Lock(_p_mutex);
- _ring[_p_step++] = in;
- _p_step %= _cap;
- UnlocK(_p_mutex);
- V(_data_sem);//V操作 ++
-
- }
-
- void pop(T* out)//消费
- {
- //信号量存在的意义
- //1.可以不用在临界区内部对资源做判断,就可以知道临界资源的使用情况
- //2.什么时候用锁?什么时候用sem(信号量)?取决于你对应的临界资源,是否被整体使用!
- P(_data_sem);
- Lock(_c_mutex);
- *out = _ring[_c_step++];
- _c_step %= _cap;
- UnlocK(_c_mutex);
- V(_space_sem);
- }
-
- ~RingQueue()
- {
- sem_destroy(&_data_sem);
- sem_destroy(&_space_sem);
-
- pthread_mutex_destroy(&_c_mutex);
- pthread_mutex_destroy(&_p_mutex);
- }
- private:
- vector<T> _ring;
- int _cap;//环形队列容器大小
- sem_t _data_sem;//数据 -- 只有消费者关心
- sem_t _space_sem;//空间 -- 只有生产者关心
- int _c_step;//消费位置
- int _p_step;//生产位置
-
- //因为消费者和生产者是并行的所以需要两把锁,防止消费者内部的竞争和生产者内部的竞争
- //因为信号量的原因并不用担心死锁问题,因为消费者和生产者指向同一个位置时,只有其中一个可以运行
- pthread_mutex_t _c_mutex;//消费者之间的锁
- pthread_mutex_t _p_mutex;//生产者之间的锁
- };

- //Thread.hpp
- #pragma once
-
- #include <iostream>
- #include <string>
- #include <cstdlib>
- #include <pthread.h>
- #include <unistd.h>
- #include <functional>
-
- using namespace std;
-
-
- class Thread
- {
- public:
- typedef enum
- {
- NEW = 0,
- RUNNING,
- EXITED
- } ThreadStatus;
- //typedef void (*func_t)(void*);
- using func_t = function<void ()>;
-
- public:
- Thread(int num, func_t func) : _tid(0), _status(NEW), _func(func)
- {
- char name[128];
- snprintf(name, sizeof(name), "thread-%d", num);
- _name = name;
- }
-
- int status() { return _status; }//获取线程状态
-
- std::string threadname() { return _name; }//获取线程名字
-
- pthread_t threadid()//获取线程ID
- {
- if (_status == RUNNING)
- return _tid;
- else
- {
- return 0;
- }
- }
-
- // runHelper是不是类的成员函数,而类的成员函数,具有默认参数this,需要static
- // 但是会有新的问题:static成员函数,无法直接访问类属性和其他成员函数
- static void* runHelper(void *args)
- {
- Thread *ts = (Thread*)args; // 就拿到了当前对象
- // _func(_args);
- (*ts)();
- return nullptr;
- }
-
- void operator ()() //仿函数
- {
- if(_func != nullptr)
- _func();
- }
-
- void run()
- {
- int n = pthread_create(&_tid, nullptr, runHelper, this);
- if(n != 0) exit(1);
- _status = RUNNING;
- }
-
- void join()
- {
- int n = pthread_join(_tid, nullptr);
- if( n != 0)
- {
- std::cerr << "main thread join thread " << _name << " error" << std::endl;
- return;
- }
- _status = EXITED;
- }
-
- ~Thread()
- {}
-
- private:
- pthread_t _tid;
- std::string _name;
- func_t _func; // 线程未来要执行的回调
- ThreadStatus _status;
- };

- //err.hpp
- #pragma once
-
- enum
- {
- USAGE_ERR = 1,
- SOCKET_ERR,
- BIND_ERR,
- };
- //lockGuard.hpp
- #pragma once
-
- #include <iostream>
- #include <pthread.h>
-
- class Mutex // 自己不维护锁,有外部传入
- {
- public:
- Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
- {}
- void lock()
- {
- pthread_mutex_lock(_pmutex);//加锁
- }
- void unlock()
- {
- pthread_mutex_unlock(_pmutex);//解锁
- }
- ~Mutex()
- {}
- private:
- pthread_mutex_t *_pmutex;
- };
-
- class LockGuard // 自己不维护锁,有外部传入
- {
- public:
- LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
- {
- _mutex.lock();//通过构造函数自动加锁,防止忘记加锁
- }
- ~LockGuard()
- {
- _mutex.unlock();//通过析构函数自动解锁,防止忘记解锁
- }
- private:
- Mutex _mutex;
- };

listen 函数是在套接字编程中用于监听客户端连接请求的函数。它被用于服务器端,用于标识一个套接字以侦听客户端的连接请求。
函数原型如下:
- #include <sys/types.h>
- #include <sys/socket.h>
-
- int listen(int sockfd, int backlog);
返回值:
listen 函数通常用于TCP套接字,在调用 listen 之前,通常需要通过 socket 创建套接字并使用 bind 绑定到一个地址。一旦调用 listen,套接字就处于监听状态,可以通过 accept 函数接受来自客户端的连接请求。
示例:
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <arpa/inet.h>
- #include <sys/types.h>
- #include <sys/socket.h>
-
- int main() {
- int server_fd, client_fd;
- struct sockaddr_in server_addr, client_addr;
- int backlog = 5;
-
- // 创建 socket
- server_fd = socket(AF_INET, SOCK_STREAM, 0);
- if (server_fd == -1) {
- perror("Socket creation failed");
- exit(EXIT_FAILURE);
- }
-
- // 绑定地址
- server_addr.sin_family = AF_INET;
- server_addr.sin_addr.s_addr = INADDR_ANY;
- server_addr.sin_port = htons(8080);
- if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
- perror("Bind failed");
- exit(EXIT_FAILURE);
- }
-
- // 监听连接
- if (listen(server_fd, backlog) == -1) {
- perror("Listen failed");
- exit(EXIT_FAILURE);
- }
-
- printf("Server listening on port 8080...\n");
-
- // 其他处理...
-
- close(server_fd);
-
- return 0;
- }

在这个例子中,listen 函数用于监听客户端连接请求,指定了最大等待队列的长度。接下来,服务器可以使用 accept 函数接受连接请求,处理客户端连接。
netstat 是一个用于显示网络状态信息的命令行工具,在 Linux 系统中广泛使用。它可以显示各种网络相关信息,比如网络连接、路由表、接口统计等。netstat 可以用于查看活动连接、监听端口、路由表等网络相关信息,帮助诊断网络问题和监视网络活动。
在使用 netstat 时,常见的一些选项包括:
例如,要显示所有的网络连接和监听的端口,可以使用以下命令:
netstat -a
如果只想显示 TCP 连接,可以使用以下命令:
netstat -t
netstat 在最新的 Linux 发行版中可能已被标记为过时,建议使用 ss 命令或 ip 工具来代替。例如,ss 命令可以用来显示套接字统计信息。
ss -t -a
无论是 netstat 还是 ss,它们都是很有用的网络工具,可以帮助了解系统中的网络连接情况、诊断网络问题并监控网络活动。
accept 函数用于从监听套接字接受连接,并返回一个新的套接字描述符,该文件描述符(套接字)用于与客户端进行通信。这个新的套接字是专门为与新连接的客户端通信而创建的。accept 函数通常在服务器程序中使用,用于接受客户端的连接请求。
函数原型如下:
- #include <sys/types.h>
- #include <sys/socket.h>
-
- int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept 函数的返回值是一个新的套接字描述符,用于与客户端进行通信。如果出现错误,accept 返回 -1,并设置 errno 表示错误的类型。
下面是一些可能的错误情况:
注:accept 函数是一个阻塞调用。在TCP服务器编程中,accept 用于接受客户端的连接请求,并创建一个新的套接字用于与客户端通信。当没有连接请求到来时,accept 会一直阻塞等待,直到有新的连接请求到达为止。
以下是一个简单的使用 accept 的示例:
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <arpa/inet.h>
-
- int main() {
- int server_fd, client_fd;
- struct sockaddr_in server_addr, client_addr;
- socklen_t client_len = sizeof(client_addr);
-
- // 创建 socket
- server_fd = socket(AF_INET, SOCK_STREAM, 0);
- if (server_fd == -1) {
- perror("Socket creation failed");
- exit(EXIT_FAILURE);
- }
-
- // 绑定地址
- server_addr.sin_family = AF_INET;
- server_addr.sin_addr.s_addr = INADDR_ANY;
- server_addr.sin_port = htons(8080);
- if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
- perror("Bind failed");
- exit(EXIT_FAILURE);
- }
-
- // 监听连接
- if (listen(server_fd, 5) == -1) {
- perror("Listen failed");
- exit(EXIT_FAILURE);
- }
-
- printf("Server listening on port 8080...\n");
-
- // 接受连接
- client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
- if (client_fd == -1) {
- perror("Accept failed");
- exit(EXIT_FAILURE);
- }
-
- printf("Connection accepted from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
-
- // 在新的 client_fd 上进行通信
-
- close(server_fd);
- close(client_fd);
-
- return 0;
- }

在这个例子中,accept 函数用于接受来自客户端的连接请求,返回一个新的套接字描述符 client_fd,通过这个描述符可以进行与客户端的通信。
注:socket函数和accept 函数的返回值都是返回一个套接字,他们两个的返回值之间的区别:
socket 的返回值:
socket 函数用于创建一个新的套接字,它返回一个套接字描述符(socket descriptor)。
套接字描述符是一个整数值,代表着操作系统内核中关联的套接字资源。这个描述符是后续进行套接字通信的标识符。简单来说socket 函数创建的套接字是用来标识IP地址类型和端口号(端口号通过bing函数写入)以及通信类型方式的。
地址类型:
通常使用 AF_INET (IPv4)地址类型 或者 AF_INET6(IPv6)地址类型。
通信类型:
通过指定 SOCK_STREAM,创建的是一个面向连接的套接字,即TCP套接字。TCP提供可靠的、有序的、双向的字节流通信。
通过指定 SOCK_DGRAM 用于套接字类型,那么创建的将是一个用于UDP通信的套接字,UDP是无连接的、不可靠的通信协议。
accept 的返回值:
accept 函数用于在服务器端接受客户端的连接请求,并创建一个新的套接字用于与该客户端通信。accept 函数返回一个新的套接字描述符(返回值),用于实际的数据传输,与客户端进行通信。通过IO函数可以对该 描述符 进行 读写 操作。
简单举一个例子描述 socket 和 accept 创建的套接字的区别:
假设有一家餐厅,有一个专门负责揽客的店员叫做张三和一堆服务员李四、王五等,张三负责在店外招揽客人进餐厅吃饭,李四、王五负责给客人进行点菜和上菜等服务。
所以,可以将整个过程类比为张三(listensocket)在餐厅外招揽客人,当有客人进来时,由服务员(accept 创建的新套接字)负责与客人进行具体的点菜和上菜服务。这样的比喻可以帮助理解服务器端套接字和客户端套接字之间的区别及其各自的角色。
总结两者之间的区别:
socket创建的套接字是用来接受其他主机发送过来的通信连接,当然也可以用于服务器和客户端的通信,而accept创建的套接字是用来服务器和客户端的通信的。
在服务器端,调用socket函数创建一个监听套接字,用于监听等待客户端的连接请求。然后,通过调用accept函数,创建一个新的套接字,用于与具体的客户端进行通信(accept:用于个客户端发送请求连接的时候)。socket函数创建的套接字也可以用于服务器和客户端之间的通信(socket:用于一个客户端发送请求连接的时候),所以socket创建的套接字在一对一的情况下是用来作为通信套接字,一对多的情况下是用来作为监听套接字。
在客户端,调用socket函数创建一个套接字,用于与服务器建立通信连接。通过这个套接字,客户端可以向服务器发送请求并接收服务器的响应。
总结来说,socket是通用的用于通信的数据结构,而accept创建的套接字是服务器与客户端之间的通信套接字。
connect函数是用于建立TCP连接的系统调用(或函数),通常在TCP客户端中使用。在C语言中,connect函数的原型如下:
- #include <sys/types.h>
- #include <sys/socket.h>
-
- int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
在使用connect函数时,需要提供以下参数:
connect函数的作用是尝试与指定的目标服务器建立连接。它完成了以下主要步骤:
在使用connect函数之前,通常需要先创建套接字并使用bind函数绑定本地地址(如果需要)。connect函数通常在TCP客户端的初始化过程中调用,以建立与服务器的连接,然后客户端可以使用send和recv等函数进行数据交换。
send 函数通常用于在面向连接的套接字(如TCP套接字)上发送数据。它是一种可靠的、有序的数据传输方式。
- #include <sys/socket.h>
-
- ssize_t send(int sockfd, const void *buf, size_t len, int flags);
返回值:
需要注意的是,send 函数是在面向连接的套接字上使用的,通常用于 TCP 协议。在使用 send 函数之前,通常需要先使用 connect 函数建立连接。
在某些情况下,你可以使用 write 函数替代 send 函数,特别是在使用面向连接的套接字(如 TCP 套接字)时。然而,需要注意的是,它们有一些细微的区别。
通常recv函数用于TCP套接字。recv 函数用于从已连接的套接字接收数据。它的原型如下:
- #include <sys/types.h>
- #include <sys/socket.h>
-
- ssize_t recv(int sockfd, void *buf, size_t len, int flags);
其中:
recv 函数的返回值表示实际接收到的字节数,可能的情况包括:
在调用 recv 函数后,应用程序通常会检查返回值并根据返回值的不同情况来处理接收到的数据或处理错误。
注:
send函数是真的把我们定义的缓冲区里面的数据直接发送到对方的主机吗?显然不是,在Linux内核中存在两个缓冲区,一个是发送缓冲区,另一个是接受缓冲区,在发送数据时,TCP协议要先把send发送的数据拷贝到内核的发送缓冲区,然后再把数据发送到对方主机内核的接受缓冲区,而recv函数也不是直接把数据读取上来,要等接受缓冲区把数据拷贝到recv函数提供的缓冲区内才可以进行读取数据。
这个过程涉及几个关键步骤:
inet_aton函数是用于将点分十进制(IPv4地址的一种表示形式)转换为网络地址的函数(也是主机序列转换网络序列)。该函数在C语言中常常用于网络编程中,特别是在套接字编程中。
inet_aton函数的原型如下:
- #include <arpa/inet.h>
-
- int inet_aton(const char *cp, struct in_addr *inp);
函数的参数解释如下:
inet_aton函数的返回值是一个整数,如果转换成功,返回1,如果失败,返回0。在失败的情况下,可以使用errno变量来获取错误的具体原因。
在 Unix 和 Unix-like 操作系统中(如 Linux),jobs 命令用于显示当前 shell 中的作业列表。作业(进程/任务)是由 shell 管理的进程或命令的集合。(从现在开始,以后启动的进程我们都叫做进程组或者任务)以下是 jobs 命令的基本用法:
jobs [-lnprs] [jobspec ...] or jobs -x command [args]
示例:
jobs
jobs -l
jobs -s
- sleep 100 &
- jobs
jobs 命令对于在后台运行的作业的管理非常有用,特别是在使用交互式 shell 时。通过 jobs,用户可以查看当前 shell 中正在运行或已停止的作业,以及它们的状态和标识符。
注:每个进程组(Process Group)都有一个进程组组长(Process Group Leader)。进程组组长是该进程组中第一个创建的进程。一旦创建了进程组,它的进程组组长就不能改变。
进程组组长的主要职责是接收来自终端的信号,例如中断信号(Ctrl+C)。当用户在终端上按下中断键时,中断信号会被发送给整个进程组的每个成员,然后由进程组组长接收并处理。这是为了确保用户能够方便地中断整个作业或进程组。
在创建新的进程组时,通常第一个进程会成为该进程组的组长。如果你在终端上启动一个新的命令,该命令就会成为一个新的进程组,而该命令的第一个进程将成为进程组组长。
1.在会话中只能有一个前台任务在运行!我们在命令行启动一个进程的时候,bash无法运行了!
2.如果登录就是创建一个会话,bash任务(启动的进程),就是在当前会话中创建新的前台任务,那么如果我们退出呢?就是销毁会话,可能会影响会话内部的所有任务!所以为了不受到用户的登录和注销的影响,一般网络服务器,都是以守护进程的方式进行运行!(只要我们的电脑(云服务器)没有关闭,服务器依然可以在运行)
举个例子:
一个公司只有一个上级领导,而这个领导手下有一堆人,划分几个人为一组,而每个小组都要有自己的组长。
在 Linux 中,setsid 函数用于创建一个新的会话(session)并设置调用进程为该会话的领头进程(session leader)。这样,调用进程就成为一个新会话中的唯一进程组的领头进程,并且脱离了原来的控制终端。
具体的函数原型如下:
- #include <unistd.h>
-
- pid_t setsid(void);
setsid()函数在调用进程中创建一个新的会话,并使其成为进程组的组长。该函数返回新会话的ID。
使用setsid()函数时,有以下几个注意点:
常见用途:
示例用法
- #include <unistd.h>
- #include <stdio.h>
-
- int main() {
- pid_t pid;
-
- pid = fork();
- if (pid < 0) {
- perror("fork");
- return -1;
- } else if (pid > 0) {
- // 父进程退出
- return 0;
- }
-
- // 子进程调用setsid()函数创建新会话
- pid_t sid = setsid();
- if (sid < 0) {
- perror("setsid");
- return -1;
- }
-
- // 在新会话中进行其他操作
- printf("New session ID: %d\n", sid);
- // ...
-
- return 0;
- }

在上述示例中,首先调用fork()创建子进程,然后在子进程中调用setsid()函数创建新会话。随后,在新会话中,可以进行需要的操作,如设置工作目录、关闭标准输入输出等。
注:守护进程:a. 要忽略异常信号 b. 0,1,2,要做特殊处理 c. 进程的工作路径可能要更改。
/dev/null 是一个特殊的设备文件,用于丢弃数据。在 Unix 和类 Unix 系统中,/dev/null 被称为空设备,任何写入它的数据都会被丢弃,而读取它则会立即得到文件结束符。
这个设备在很多情况下用于丢弃输出或者创建空输入。以下是一些常见用途:
some_command > /dev/null
some_command < /dev/null
some_command > /dev/null 2>&1
这个例子中,2>&1 将标准错误(stderr)重定向到与标准输出(stdout)相同的位置,然后将标准输出重定向到 /dev/null。
总的来说,/dev/null 提供了一种方便的方法来处理输入或输出,使其对系统的影响最小。
- //tcpClient.cpp
- #include <iostream>
- #include <string>
- #include <cstring>
- #include <sys/types.h> /* See NOTES */
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- #include "err.hpp"
- //因为没有进行模块分离所以没有tcpClient.hpp的头文件
- using namespace std;
-
- static void usage(std::string proc)
- {
- std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
- << std::endl;
- }
-
- // ./tpc_client serverip serverport
- int main(int argc, char *argv[])
- {
- // 准备工作
- if(argc != 3)
- {
- usage(argv[0]);
- exit(USAGE_ERR);
- }
- string serverip = argv[1];
- uint16_t serverport = atoi(argv[2]);
-
- // 1. create socket
- int sock = socket(AF_INET, SOCK_STREAM, 0);
- if(sock < 0)
- {
- cerr << "socket error : " << strerror(errno) << endl;
- exit(SOCKET_ERR);
- }
-
- // 客户端要不要bind? 要
- // 客户端要不要自己bind? 不要,因为client要让OS自动给用户进行bind,这里和udp一样
- // 客户端要不要listen?不要 要不要accept?不需要,因为客户端永远都是连别人的
- // 明确server是谁
- struct sockaddr_in server;
- memset(&server, 0, sizeof(server));
- server.sin_family = AF_INET;
- server.sin_port = htons(serverport);
- inet_aton(serverip.c_str(), &(server.sin_addr));
-
- // 2. connect -- 把客户端和服务器建立连接
- int cnt = 5;
- while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0)
- {
- sleep(1);
- cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl;
- if(cnt <= 0) break;
- }
- if(cnt <= 0)
- {
- cerr << "连接失败..." << endl;
- exit(CONNECT_ERR);
- }
- char buffer[1024];
- // 3. 连接成功
- while(true)
- {
- string line;
- cout << "Enter>>> ";
- getline(cin, line);
-
- write(sock, line.c_str(), line.size());
-
- ssize_t s = read(sock, buffer, sizeof(buffer)-1);
- if(s > 0)
- {
- buffer[s] = 0;
- cout << "server echo >>>" << buffer << endl;
- }
- else if(s == 0)
- {
- cerr << "server quit" << endl;
- break;
- }
- else {
- cerr << "read error: " << strerror(errno) << endl;
- break;
- }
- }
- close(sock);
-
- return 0;
- }

- //tcpServer.cpp
- #include "tcpServer.hpp"
- #include "daemon.hpp"
- #include <memory>
-
- using namespace std;
- using namespace ns_server;
-
- static void usage(string proc)
- {
- std::cout << "Usage:\n\t" << proc << " port\n"
- << std::endl;
- }
-
- std::string echo(const std::string& message)
- {
- return message;
- }
-
- // ./tcp_server port
- int main(int argc, char *argv[])
- {
- if(argc != 2)
- {
- usage(argv[0]);
- exit(USAGE_ERR);
- }
- uint16_t port = atoi(argv[1]);
- unique_ptr<TcpServer> tsvr(new TcpServer(echo, port));
-
- tsvr->initServer();
- //将服务器守护进程化
- Daemon();
- tsvr->start();
-
- return 0;
- }

- //tcpServer.hpp
- #pragma once
-
- #include <iostream>
- #include <cstdlib>
- #include <cstring>
- #include <functional>
- #include <sys/types.h> /* See NOTES */
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/wait.h>
- #include <signal.h>
- #include <pthread.h>
- #include "err.hpp"
- #include "Thread.hpp"
- #include "Task.hpp"
- #include "ThreadPool.hpp"
- #include "log.hpp"
-
- namespace ns_server
- {
- static const uint16_t defaultport = 8081;
- static const int backlog = 32; //? TODO
-
- using func_t = std::function<std::string(const std::string &)>;
-
- class TcpServer;//声明
- class ThreadData
- {
- public:
- ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer *ts)
- : sock(fd), clientip(ip), clientport(port), current(ts)
- {}
- public:
- int sock;
- std::string clientip;
- uint16_t clientport;
- TcpServer *current;
- };
-
- class TcpServer
- {
- public:
- TcpServer(func_t func, uint16_t port = defaultport) : func_(func), port_(port), quit_(true)
- {
- }
- void initServer()
- {
- // 1. 创建socket, 文件
- listensock_ = socket(AF_INET, SOCK_STREAM, 0);//SOCK_STREAM:创建流套接字
- if (listensock_ < 0)
- {
- // std::cerr << "create socket error" << std::endl;
- logMessage(Fatal, "create socket error, code: %d, error string: %s", errno, strerror(errno));
- exit(SOCKET_ERR);
- }
- // 2. bind
- struct sockaddr_in local;
- memset(&local, 0, sizeof(local));
- local.sin_family = AF_INET;
- local.sin_port = htons(port_);
- local.sin_addr.s_addr = htonl(INADDR_ANY);
- if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
- {
- //std::cerr << "bind socket error" << std::endl;
- logMessage(Fatal, "bind socket error, code: %d, error string: %s", errno, strerror(errno));
- exit(BIND_ERR);
- }
- // 3. 监听
- if (listen(listensock_, backlog) < 0)
- {
- //std::cerr << "listen socket error" << std::endl;
- logMessage(Fatal, "listen socket error, code: %d, error string: %s", errno, strerror(errno));
- exit(LISTEN_ERR);
- }
- }
- void start()
- {
- //signal(SIGCHLD, SIG_IGN); //方法2:ok, 推荐的
- // signal(SIGCHLD, handler); //方法3:还行,不太推荐
-
- quit_ = false;
- while (!quit_)
- {
- struct sockaddr_in client;
- socklen_t len = sizeof(client);
- // 4. 获取连接,accept
- int sock = accept(listensock_, (struct sockaddr *)&client, &len);
- if (sock < 0)
- {
- // std::cerr << "accept error" << std::endl;
- logMessage(Warning, "accept error, code: %d, error string: %s", errno, strerror(errno));
- continue;
- }
-
- // 提取client信息 -- debug
- std::string clientip = inet_ntoa(client.sin_addr);
- uint16_t clientport = ntohs(client.sin_port);
-
- // 5. 获取新连接成功, 开始进行业务处理
- logMessage(Info, "获取新连接成功: %d from %d, who: %s - %d",
- sock, listensock_, clientip.c_str(), clientport);
- // std::cout << "获取新连接成功: " << sock << " from " << listensock_ << ", "
- // << clientip << "-" << clientport << std::endl;
-
- // v1
- //service(sock, clientip, clientport);//如果让服务器主进程和客户端进行通信,则无法和其他客户端建立连接,一次只能和一个客户端通信,所以有了以下其他版本
-
- // //v2: 多进程版本
- // pid_t id = fork();
- // if (id < 0)
- // {
- // close(sock);
- // continue;
- // }
- // else if (id == 0) // child, 父进程的fd,会被child继承吗?会。 父子会用同一张文件描述符表吗?不会,但是子进程拷贝继承父进程的fd table(表);
- // {
- // // 建议关闭掉不需要的fd
- // close(listensock_);
- // if(fork() > 0) exit(0); //方法4:就这一行代码,不太推荐,因为调用的fork太多了对系统有一定的要求,但是具有一定的设计意义,所以还是推荐方法2
- // // child已经退了,孙子进程在运行。虽然方法父进程一样会被阻塞,但是不会被阻塞太久,因为子进程调用fork之后就开始退出了,没有进行其他的操作,剩下事前交给孙子进程
- // //孙子进程的父进程先退出了,就变成了孤儿进程,所以孙子进程会被系统领养,由系统承担子进程的回收工作。
- // service(sock, clientip, clientport);
- // exit(0);
- // }
-
- // // 父, 一定关闭掉不需要的fd, 不关闭,会导致fd泄漏
- // close(sock);
- // pid_t ret = waitpid(id, nullptr, 0); //阻塞的! waitpid(id, nullptr, WNOHANG);//方法1:不推荐,因为 accept 是阻塞式调用!都没有新客户端的请求连接时就会进行阻塞导致
- // //父进程就无法继续向后执行,当有子进程退出时就无法执行waitpid进行回收子进程
- // if(ret == id) std::cout << "wait child " << id << " success" << std::endl;
-
- // v3: 多线程 -- 原生多线程
- // 1. 要不要关闭不要的socket??不能
- // 2. 要不要回收线程?如何回收?会不会阻塞??
- pthread_t tid;
- ThreadData *td = new ThreadData(sock, clientip, clientport, this);
- pthread_create(&tid, nullptr, threadRoutine, td);
-
- // v4: v3版一旦用户来了,你才创建线程,这样效率是不是有点低?可以使用线程池来解决问题
- // 使用线程池的时候,一定是有线的线程个数,一定是要处理短任务
- Task t(sock, clientip, clientport, std::bind(&TcpServer::service, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
- ThreadPool<Task>::getinstance()->pushTask(t);
- }
- }
-
- static void *threadRoutine(void *args)//类内线程的回调函数要设置为static,防止第一个参数被this指针占用
- {
- pthread_detach(pthread_self());//分离
-
- ThreadData *td = static_cast<ThreadData *>(args);
- td->current->service(td->sock, td->clientip, td->clientport);
- delete td;
- return nullptr;
- }
-
- void service(int sock, const std::string &clientip, const uint16_t &clientport)
- {
- std::string who = clientip + "-" + std::to_string(clientport);
- char buffer[1024];
-
- //v2
- ssize_t s = read(sock, buffer, sizeof(buffer)-1);
- if(s > 0)
- {
- buffer[s] = 0;
- std::string res = func_(buffer);
- std::cout << who << ">>>" << res << std::endl;
- logMessage(Debug, "%s# %s", who.c_str(), res.c_str());
- write(sock, res.c_str(), res.size());
- }
- else if(s == 0)
- {
- // 对方将连接关闭了
- // std::cout << who << " quit, me too" << std::endl;
- logMessage(Info, "%s quit,me too", who.c_str());
- }
- else
- {
- // std::cerr << "read error: " << strerror(errno) << std::endl;
- logMessage(Error, "read error, %d:%s", errno, strerror(errno));
- }
- close(sock);
-
- //v1
- // while(true)
- // {
- // ssize_t s = read(sock, buffer, sizeof(buffer)-1);
- // if(s > 0)
- // {
- // buffer[s] = 0;
- // std::string res = func_(buffer);
- // std::cout << who << ">>>" << res << std::endl;
- // }
- // else if(s == 0)
- // {
- // //对方将连接关闭了
- // close(sock);
- // std::cout << who << " quit, me too" << std::endl;
- // break;
- // }
- // else
- // {
- // close(sock);
- // std::cerr << "read error: " << strerror(errno) << std::endl;
- // break;
- // }
- // }
- }
-
- ~TcpServer()
- {
- }
-
- private:
- uint16_t port_;
- int listensock_; // TODO
- bool quit_;
- func_t func_;
- };
- }

- //Makefile
- .PHONY:all
- all: tcp_client tcp_server
-
- tcp_client:tcpClient.cc
- g++ -o $@ $^ -std=c++11 -lpthread
-
- tcp_server:tcpServer.cc
- g++ -o $@ $^ -std=c++11 -lpthread
-
- .PHONY:clean
- clean:
- rm -f tcp_client tcp_server
- //Task.hpp
- #pragma once
- #include <iostream>
- #include <string>
- #include <unistd.h>
- #include <functional>
-
- using cb_t = std::function<void(int , const std::string &, const uint16_t &)>;
-
- class Task
- {
- public:
- Task()
- {
- }
- Task(int sock, const std::string &ip, const uint16_t &port, cb_t cb)
- : _sock(sock), _ip(ip), _port(port), _cb(cb)
- {}
- void operator()()
- {
- _cb(_sock, _ip, _port);
- }
- ~Task()
- {
- }
-
- private:
- int _sock;
- std::string _ip;
- uint16_t _port;
- cb_t _cb;
- };

- //Thread.hpp
- #pragma once
-
- #include <iostream>
- #include <string>
- #include <cstdlib>
- #include <pthread.h>
- #include <unistd.h>
-
- using namespace std;
-
-
- class Thread
- {
- public:
- typedef enum
- {
- NEW = 0,
- RUNNING,
- EXITED
- } ThreadStatus;
- typedef void (*func_t)(void*);
-
- public:
- Thread(int num, func_t func, void *args) : _tid(0), _status(NEW), _func(func), _args(args)
- {
- char name[128];
- snprintf(name, sizeof(name), "thread-%d", num);
- _name = name;
- }
-
- int status() { return _status; }//获取线程状态
-
- std::string threadname() { return _name; }//获取线程名字
-
- pthread_t threadid()//获取线程ID
- {
- if (_status == RUNNING)
- return _tid;
- else
- {
- return 0;
- }
- }
-
- // runHelper是不是类的成员函数,而类的成员函数,具有默认参数this,需要static
- // 但是会有新的问题:static成员函数,无法直接访问类属性和其他成员函数
- static void *runHelper(void *args)
- {
- Thread *ts = (Thread*)args; // 就拿到了当前对象
- // _func(_args);
- (*ts)();
- return nullptr;
- }
-
- void operator ()() //仿函数
- {
- if(_func != nullptr)
- _func(_args);
- }
-
- void run()
- {
- int n = pthread_create(&_tid, nullptr, runHelper, this);
- if(n != 0) exit(1);
- _status = RUNNING;
- }
-
- void join()
- {
- int n = pthread_join(_tid, nullptr);
- if( n != 0)
- {
- std::cerr << "main thread join thread " << _name << " error" << std::endl;
- return;
- }
- _status = EXITED;
- }
-
- ~Thread()
- {}
-
- private:
- pthread_t _tid;
- std::string _name;
- func_t _func; // 线程未来要执行的回调
- void* _args;
- ThreadStatus _status;
- };

- //ThreadPool.hpp
- #pragma once
-
- #include <iostream>
- #include <string>
- #include <vector>
- #include <queue>
- #include <unistd.h>
- #include "Thread.hpp"
- #include "Task.hpp"
- #include "lockGuard.hpp"
- #include "log.hpp"
-
- const static int N = 5;
-
- //单例版的线程池
- template <class T>
- class ThreadPool
- {
- private:
- ThreadPool(int num = N) : _num(num)
- {
- pthread_mutex_init(&_lock, nullptr);
- pthread_cond_init(&_cond, nullptr);
- }
- ThreadPool(const ThreadPool<T> &tp) = delete;
- void operator=(const ThreadPool<T> &tp) = delete;
-
- public:
- static ThreadPool<T> *getinstance()
- {
- if(nullptr == instance) // 为什么要这样?提高效率,减少加锁的次数!
- {
- LockGuard lockguard(&instance_lock);
- if (nullptr == instance)
- {
- logMessage(Debug, "线程池单例形成");
- instance = new ThreadPool<T>();
- instance->init();
- instance->start();
- }
- }
-
- return instance;
- }
-
- pthread_mutex_t *getlock()
- {
- return &_lock;
- }
- void threadWait()
- {
- pthread_cond_wait(&_cond, &_lock);
- }
- void threadWakeup()
- {
- pthread_cond_signal(&_cond);
- }
- bool isEmpty()
- {
- return _tasks.empty();
- }
- T popTask()
- {
- T t = _tasks.front();
- _tasks.pop();
- return t;
- }
- static void threadRoutine(void *args)
- {
- // pthread_detach(pthread_self());
- ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
- while (true)
- {
- // 1. 检测有没有任务
- // 2. 有:处理
- // 3. 无:等待
- // 细节:必定加锁
- T t;
- {
- LockGuard lockguard(tp->getlock());
- while (tp->isEmpty())
- {
- tp->threadWait();
- }
- t = tp->popTask(); // 从公共区域拿到私有区域
- }
- // for test
- t();
- }
- }
- void init()
- {
- for (int i = 0; i < _num; i++)
- {
- _threads.push_back(Thread(i, threadRoutine, this));
- logMessage(Debug, "%d thread running", i);
- }
- }
- void start()
- {
- for (auto &t : _threads)
- {
- t.run();
- }
- }
- void check()
- {
- for (auto &t : _threads)
- {
- std::cout << t.threadname() << " running..." << std::endl;
- }
- }
- void pushTask(const T &t)
- {
- LockGuard lockgrard(&_lock);
- _tasks.push(t);
- threadWakeup();
- }
- ~ThreadPool()
- {
- for (auto &t : _threads)
- {
- t.join();
- }
- pthread_mutex_destroy(&_lock);
- pthread_cond_destroy(&_cond);
- }
-
- private:
- std::vector<Thread> _threads;
- int _num;
-
- std::queue<T> _tasks; // 使用stl的自动扩容的特性
-
- pthread_mutex_t _lock;
- pthread_cond_t _cond;
-
- static ThreadPool<T> *instance;
- static pthread_mutex_t instance_lock;
- };
-
- template <class T>
- ThreadPool<T> *ThreadPool<T>::instance = nullptr;
-
- template <class T>
- pthread_mutex_t ThreadPool<T>::instance_lock = PTHREAD_MUTEX_INITIALIZER;

- //daemon.hpp
- #pragma once
- // 1. setsid();
- // 2. setsid(), 调用进程,不能是组长!我们怎么保证自己不是组长呢?
- // 3. 守护进程a. 忽略异常信号 b. 0,1,2要做特殊处理 c. 进程的工作路径可能要更改 /
-
- #include <cstdlib>
- #include <unistd.h>
- #include <signal.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
-
- #include "log.hpp"
- #include "err.hpp"
-
- //守护进程的本质:是孤儿进程的一种!也就是父进程先退出,子进程被领养 -- 创建守护进程
- void Daemon()//自己实现一个Daemon函数,不调用库里的daemon函数来创建守护进程
- {
- // 1. 忽略信号
- signal(SIGPIPE, SIG_IGN);
- signal(SIGCHLD, SIG_IGN);
- // 2. 让自己不要成为组长
- if (fork() > 0)
- exit(0);
- // 3. 新建会话,自己成为会话的话首进程
- pid_t ret = setsid();
- if ((int)ret == -1)
- {
- logMessage(Fatal, "deamon error, code: %d, string: %s", errno, strerror(errno));
- exit(SETSID_ERR);
- }
-
- // 4. 可选:可以更改守护进程的工作路径
- // chdir("/")
-
- // 5. 处理后续的对于0,1,2的问题
- int fd = open("/dev/null", O_RDWR);
- if (fd < 0)
- {
- logMessage(Fatal, "open error, code: %d, string: %s", errno, strerror(errno));
- exit(OPEN_ERR);
- }
- dup2(fd, 0);
- dup2(fd, 1);
- dup2(fd, 2);
- close(fd);
- }

- //err.hpp
- #pragma once
-
- enum
- {
- USAGE_ERR = 1,
- SOCKET_ERR,
- BIND_ERR,
- LISTEN_ERR,
- CONNECT_ERR,
- SETSID_ERR,
- OPEN_ERR
- };
- //lockGuard.hpp
- #pragma once
-
- #include <iostream>
- #include <pthread.h>
-
- class Mutex // 自己不维护锁,有外部传入
- {
- public:
- Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
- {}
- void lock()
- {
- pthread_mutex_lock(_pmutex);//加锁
- }
- void unlock()
- {
- pthread_mutex_unlock(_pmutex);//解锁
- }
- ~Mutex()
- {}
- private:
- pthread_mutex_t *_pmutex;
- };
-
- class LockGuard // 自己不维护锁,有外部传入
- {
- public:
- LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
- {
- _mutex.lock();//通过构造函数自动加锁,防止忘记加锁
- }
- ~LockGuard()
- {
- _mutex.unlock();//通过析构函数自动解锁,防止忘记解锁
- }
- private:
- Mutex _mutex;
- };

- //log.hpp
- #pragma once
-
- #include <iostream>
- #include <string>
- #include <cstdio>
- #include <cstring>
- #include <ctime>
- #include <cstdarg>
- #include <sys/types.h>
- #include <unistd.h>
-
- //日志的实现本质就是把输出的错误描述,输出到指定的文件里,方便日后查看
- // 日志是有日志等级的
- const std::string filename = "log/tcpserver.log";
-
- enum
- {
- Debug = 0,
- Info,
- Warning,
- Error,
- Fatal,
- Uknown
- };
-
- static std::string toLevelString(int level)
- {
- switch (level)
- {
- case Debug:
- return "Debug";
- case Info:
- return "Info";
- case Warning:
- return "Warning";
- case Error:
- return "Error";
- case Fatal:
- return "Fatal";
- default:
- return "Uknown";
- }
- }
-
- static std::string getTime()
- {
- time_t curr = time(nullptr);
- struct tm *tmp = localtime(&curr);
- char buffer[128];
- snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon+1, tmp->tm_mday,
- tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
- return buffer;
- }
-
- // 日志格式: 日志等级 时间 pid 消息体
- // logMessage(DEBUG, "hello: %d, %s", 12, s.c_str()); // DEBUG hello:12, world
- void logMessage(int level, const char *format, ...)
- {
- char logLeft[1024];
- std::string level_string = toLevelString(level);
- std::string curr_time = getTime();
- snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", level_string.c_str(), curr_time.c_str(), getpid());
-
- char logRight[1024];
- va_list p;
- va_start(p, format);
- vsnprintf(logRight, sizeof(logRight), format, p);
- va_end(p);
-
- // 打印
- // printf("%s%s\n", logLeft, logRight);
-
- // 保存到文件中
- FILE *fp = fopen(filename.c_str(), "a");//a:追加
- if(fp == nullptr)return;
- fprintf(fp,"%s%s\n", logLeft, logRight);
- fflush(fp); //可写也可以不写
- fclose(fp);
-
-
- // 预备 -- 可变参数列表原理
- // va_list p; // char *
- // int a = va_arg(p, int); // 根据类型提取参数
- // va_start(p, format); //p指向可变参数部分的起始地址
- // va_end(p); // p = NULL;
- }

下图是基于TCP协议的客户端/服务器程序的一般流程:
服务器初始化:
调用socket, 创建文件描述符;
调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
调用socket, 创建文件描述符;
调用connect, 向服务器发起连接请求;
connect会发出SYN段并阻塞等待服务器应答; (第一次)
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手;
数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞待;
这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。
1. connect() 调用:
2. 阻塞的 socket 函数:
3. read() 返回 0:
4. 应用程序如何知道 TCP 协议层的状态变化:
TCP 和 UDP 对比:
可靠传输 vs 不可靠传输
有连接 vs 无连接
字节流 vs 数据报
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。