Linux C 网络编程
Linux C 网络编程
Linux C 网络编程
一、socket
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()
系统调用创建一个套接字文件,然后以文件形式来操作网络通信,创建成功会返回套接字描述符,失败返回-1并设置errno。
参数说明:
domain
:协议族,一般为AF_INET
(表示使用IPv4)或AF_INET6
(表示使用IPv6)。type
:套接字类型,用于进一步指定使用协议族中的哪个子协议来通信,确定数据的传输方式,一般考虑以下值:
a.SOCK_STREAM
表示创建面向连接的数据流套接字,基于TCP协议,传输的数据没有数据边界。
b.SOCK_DGRAM
表示创建面向消息的数据报套接字,基于UDP协议,传输的数据有数据边界。
c.SOCK_RAW
表示创建原始套接字,可以通过修改报文头、避开系统协议栈来执行一些更底层的操作,比如处理ICMP、IGMP等网络报文。
d.SOCK_NONBLOCK
表示将socket返回的文件描述符指定为非阻塞的,它可以与前面的宏进行或运算。protocol
:传输协议,一般情况下可以直接写0
,由操作系统自动推演出应该使用什么协议。当然我们也可以通过IPPROTO_TCP
或IPPROTO_UDP
等参数值来显示指定协议。
二、bind
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
typedef unsigned short int sa_family_t;
typedef unsigned short int in_port_t;
typedef unsigned int in_addr_t;
struct sockaddr {
sa_family_t sa_family; // 16位地址族
char sa_data[14]; // 14字节地址信息,包括IP地址和端口号,剩余部分填充0
}
struct sockaddr_in {
sa_family_t sin_family; // 16位地址族
in_port_t sin_port; // 16位端口号
struct in_addr sin_addr; // 32位IP地址
unsigned char sin_zero[8]; // 8字节的填充0,用于保证sockaddr_in的大小与sockaddr一致
};
struct in_addr {
in_addr_t s_addr; // 32位IPV4地址
};
bind()
系统调用将指定了通信协议的套接字文件与IP以及端口绑定起来。
参数说明:
sockfd
:套接字的文件描述符。addr
:指定要绑定的参数信息。编程中一般并不直接针对sockaddr数据结构操作,而是使用与sockaddr等价的sockaddr_in数据结构。addrlen
:第二个参数的结构的长度。
此外,由于网络字节序有一般采用大端(高尾端)排序方式,所以从主机向网络发送和从网络向主机接收时要借助于以下四个库函数实现字节序的转换:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // host to network short
uint16_t htons(uint16_t hostshort); // host to network short
uint32_t ntohl(uint32_t netlong); // network to host long
uint16_t ntohs(uint16_t netshort); // network to host short
同时,IP地址的格式也要在字符串和32位整数之间相互转化:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp); // 将点分十进制IP地址转换成网络字节序IP地址
in_addr_t inet_addr(const char *cp); // 将点分十进制IP地址转换成网络字节序IP地址
in_addr_t inet_network(const char *cp); // 将点分十进制IP地址转换成主机字节序IP地址
char *inet_ntoa(struct in_addr in); // 将网络字节序IP地址转换成点分十进制IP地址
因此,服务器套接字信息的初始化过程大致如下:
#define SERVER_PORT 1521 // 服务器程序端口
#define SERVER_IP "0.0.0.0" // 服务器程序IP地址
/* 初始化服务器套接字信息 */
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET; // 使用IPv4
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 对IP地址进行格式转化
server_addr.sin_port = htons(SERVER_PORT); // 对端口进行字节序转化
三、getsockopt 和 setsockopt
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
getsockopt()
和 setsockopt()
这两个系统调用分别用于读取(Get)和设置(Set)套接字的诸多可选项。
参数说明:
sockfd
:待查看或更改的套接字文件描述符。level
:待查看或更改的套接字可选项的协议层。如SOL_SOCKET
表示套接字层相关可选项,IPPROTO_TCP
表示TCP相关可选项,IPPROTO_IP
表示IP相关可选项。optname
:待查看或更改的套接字可选项的选项名,如SO_SNDBUF
和SO_RCVBUF
可选项表示输入输出缓冲区大小的相关可选项,SO_REUSEADDR
表示套接字端口号可重用可选项。optval
:保存查看结果或修改信息的地址。optlen
:表示optval
参数指向的缓冲区的大小。此参数在getsockopt()
中会作为一个值-结果参数(value-result argument),因此需要传递地址。
1.查看输入输出缓冲区大小
创建套接字的同时将在内核生成I/O缓冲,通过 SO_SNDBUF
和 SO_RCVBUF
参数读取和修改当前I/O缓冲区的大小。
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
int main(int argc, char *argv[]) {
int sockfd;
int snd_buf_len, rcv_buf_len;
socklen_t len;
/* 获取套接字文件描述符 */
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket error");
exit(1);
}
/* 获取输出缓冲区大小 */
len = sizeof(snd_buf_len);
if (getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf_len, &len) == -1) {
perror("getsockopt error");
exit(1);
}
printf("snd_buf_len : %dKB.\n", snd_buf_len/1024);
/* 获取输入缓冲区大小 */
len = sizeof(rcv_buf_len);
if (getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf_len, &len) == -1) {
perror("getsockopt error");
exit(1);
}
printf("rcv_buf_len : %dKB.\n", rcv_buf_len/1024);
return 0;
}
atreus@MacBook-Pro % gcc server.c -o server
atreus@MacBook-Pro % ./server
snd_buf_len : 128KB.
rcv_buf_len : 128KB.
atreus@MacBook-Pro %
2.设置套接字端口号为可重用
在终止服务器和客户端的连接时,一般情况下是由客户端通过 close()
或 shutdown()
系统调用向服务器发送FIN消息,并通过四次挥手终止连接(Ctrl+C强制终止程序时,会由操作系统关闭文件和套接字,此过程也会向服务器发送FIN消息),此时一般不会发生异常。
但如果是服务器主动关闭连接,由于四次挥手过程中主动关闭方默认需要在TIME-WAIT状态下等待2MSL(Maximum Segment Life)才能将端口资源释放,这会导致此端口短时间内无法重用,因此我们需要通过 setsockopt()
系统调用将该socket对应端口号设置为可重用。
/* 设置套接字端口号为可重用 */
int option = 1;
setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));
3.设置多播数据传输
多播是基于UDP实现的,也就是说多播数据包的格式与UDP数据包相同。但与一般的UDP数据包不同,在接收到一个多播数据包后,路由器会复制该数据包并将其传递到多播组中的所有主机。
为了实现多播数据传输,对于发送方来说,需要设置数据包的TTL(Time To Live),TTL的大小设置应当适中,过大会增加网络流量,过小会使得数据包无法传递到目标主机。
/* 设定发送方发送的数据包的TTL值 */
int ttl = 32;
setsockopt(send_sockfd, IPPROTO_IP, IP_MULTICAST_TTL, (void *)&ttl, sizeof(ttl));
而对于接收方来说,为了接收多播数据包,应当将本机地址加入到相应多播组中。
/* 将本机地址加入多播组 */
struct ip_mreq multicast_group;
multicast_group.imr_multiaddr.s_addr = inet_addr("224.0.0.1"); // 多播组需要使用D类IP地址,其范围为224.0.0.0-239.255.255.255
multicast_group.imr_interface.s_addr = INADDR_ANY; // 加入多播组的主机地址信息
setsockopt(recv_sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void *)&multicast_addr, sizeof(multicast_addr));
四、listen(TCP 服务端)
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen()
系统调用将套接字文件描述符从主动文件描述符变为被动文件描述符,进入等待连接请求状态,被动监听客户的连接。
参数说明:
sockfd
:socket返回的套接字文件描述符,即希望进入等待连接请求状态的套接字文件描述符。backlog
:自Linux 2.2以后,此参数仅用于指定全连接队列的长度(保存着已经完成三次握手但还没有被应用程序通过accept()
取走的连接),但backlog
值不一定严格等于全连接队列的实际长度,其长度一般为内核中的somaxconn值与此backlog值的最小值。
五、accept(TCP 服务端)
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
accept()
系统调用受理连接请求等待队列中待处理的客户端连接请求。函数调用成功时,accep()
内部将产生用于I/O数据的套接字,并返回其文件描述符。需要注意的是,套接字是自动创建的,并自动与发起连接请求的客户端建立连接。
参数说明:
sockfd
:已经通过listen()
转化的被动服务器套接字文件描述符。addr
:用于记录发起连接请求的那个客户端的IP和端口,会由accept()
自动填充。addrlen
:一个值-结果参数(value-result argument),调用时需要将其初始化为addr指向的结构体的大小从而避免内核写越界,返回时它将包含内核写入的客户端套接字信息的实际大小。
六、connect(TCP 客户端和 UDP 客户端)
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
如果sockfd为TCP套接字描述符,connect()
系统调用会向服务器主动发起连接请求,即主动发起三次握手。
如果sockfd为UDP套接字描述符,connect()
系统调用会将该UDP套接字设置为已连接UDP套接字。使用已连接UDP套接字不需要在传输数据前注册目标IP和端口号,也不需要在传输数据完成后删除注册的目标地址信息,因此在进行连续传输是效率更高。
参数说明:
sockfd
:socket()
函数所返回的套接字文件描述符。addr
:客户端要连接的服务器程序的IP和端口。addrlen
:第二个参数所指定的结构体变量的大小。
七、send 和 recv(TCP)
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
二者均为基于TCP的I/O函数。recv()
系统调用会把内核输入缓冲区(用户
→
\to
→内核
→
\to
→网络)中的数据拷贝到应用层用户的buffer里面,并返回拷贝的字节数。send()
系统调用会把应用层buffer的数据拷贝到socket的内核输出(网络
→
\to
→内核
→
\to
→用户)缓冲区中,然后交由TCP发送。
参数说明:
sockfd
:用于通信的通信描述符,通信描述符在客户端是socket()
函数生成的套接字文件描述符,在服务端则是accept()
返回的套接字文件描述符。buf
:用户数据缓冲区,存放待发送或待接收的数据,对于有结构的数据一般需要提前进行序列化。len
:待传输的数据长度,以字节为单位。flags
:一般置为0
,表示阻塞发送或阻塞接收,如果想要使用非阻塞则置为MSG_DONTWAIT
,如果需要recv()
函数一直阻塞接收直到接收数据长度达到参数len则置为MSG_WAITALL
。
此外,TCP套接字的内核缓冲区一般还有如下特性:
- I/O缓冲在每个TCP套接字中单独存在。
- I/O缓冲在创建套接字时自动生成。
- 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
- 关闭套接字将丢失输入缓冲中的数据。
八、sendto 和 recvfrom(UDP)
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
二者均为基于UDP的I/O函数,当最后两个参数均为NULL时功能与 send()
和 recv()
相同。
参数说明:
sockfd
:用于传输数据的UDP套接字文件描述符,对于UDP来说,socket()
返回的套接字直接用于通信。buf
:用户数据缓冲区,存放待发送或待接收的数据,对于有结构的数据一般需要提前进行序列化。len
:待传输的数据长度,以字节为单位。flags
:一般置为0
,表示阻塞发送或阻塞接收,如果想要使用非阻塞则置为MSG_DONTWAIT
。dest_addr
和src_addr
:接收端和发送端的套接字信息。由于UDP不面向连接,无法自动记录对方的套接字信息,所以每次数据的发送都需要指定套接字信息。addrlen
:套接字信息结构体的大小。
九、shutdown
#include <sys/socket.h>
int shutdown(int sockfd, int how);
按照要求关闭连接,而且不管有多少个描述符指向同一个连接,shutdown()
可以一次性将其全部关闭。
参数:
sockfd
:服务端使用accept()
函数返回的描述符。how
:指定连接的断开方式,SHUT_RD
只断开读连接,SHUT_WR
只断开写连接,SHUT_RDWR
将读、写连接全部断开。
shutdown()
与 close()
的区别:
close()
只能同时关闭读写连接,而shutdown()
可以选择性关闭。close()
只能关闭单个描述符,而shutdown()
在关闭时会将与选定连接有关的全部描述符关闭。
十、gethostbyname 和 gethostbyaddr
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
#include <sys/socket.h> /* for AF_INET */
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
struct hostent {
char *h_name; // 官方域名
char **h_aliases; // 别名列表,即官方域名以外的其它域名
int h_addrtype; // IP地址的地址族信息
int h_length; // IP地址的长度
char **h_addr_list; // 域名对应的IP地址,以网络字节序形式保存
};
gethostbyname()
库函数可以通过传递字符串格式的域名获取IP地址,返回结果会被保存到hostent结构体中。而 gethostbyaddr()
库函数利用IP地址获取相关信息,同样也会将返回结果保存到hostent结构体中。
对于hostent的定义,结构体成员h_addr_list本应指向一个字符指针数组(由多个字符串构成的二维数组),但实际上每个字符指针都指向了一个in_addr结构体变量。这里之所以没有采用in_addr类型的指针是为了更好地和IPv6兼容。
参数说明:
addr
:含有IP地址信息的in_addr结构体指针。为了兼容IPv6,所以参数类型声明为了void指针。len
:第一个参数对应的地址信息的字节数,IPv4时为4
,IPv6时为16
。type
:地址族信息,IPv4时为AF_INET
,IPv6时为AF_INET6
。
1.根据域名获取主机信息
使用 gethostbyname()
获取域名对应的主机信息:
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
int main(int argc, char *argv[]) {
struct hostent *host;
/* 参数检查 */
if (argc != 2) {
printf("Command format : %s <name>!\n", argv[0]);
exit(1);
}
/* 根据域名获取其相关信息 */
if ((host = gethostbyname(argv[1])) == NULL) {
perror("gethostbyname error");
exit(1);
}
/* 打印相关信息 */
printf("official name of host : %s\n", host->h_name);
for (int i = 0; host->h_aliases[i]; i++) {
printf("alias list [%d] : %s\n", i, host->h_aliases[i]);
}
printf("host address type : %s\n", (host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
printf("length of address : %d\n", host->h_length);
for (int i = 0; host->h_addr_list[i]; i++) {
printf("list of addresses [%d], %s\n", i, inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
}
return 0;
}
atreus@MacBook-Pro % gcc server.c -o server
atreus@MacBook-Pro % ./server www.baidu.com
official name of host : www.a.shifen.com
alias list [0] : www.baidu.com
host address type : AF_INET
length of address : 4
list of addresses [0], 110.242.68.3
list of addresses [1], 110.242.68.4
atreus@MacBook-Pro %
2.根据 IP 地址获取主机信息
使用 gethostbyaddr()
获取IP地址对应的主机信息:
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
int main(int argc, char *argv[]) {
struct hostent *host;
struct in_addr addr;
/* 参数检查 */
if (argc != 2) {
printf("Command format : %s <IP>!\n", argv[0]);
exit(1);
}
/* 根据参数初始化in_addr结构体 */
addr.s_addr = inet_addr(argv[1]);
/* 根据域名获取其相关信息 */
if ((host = gethostbyaddr((void *)&addr, 4, AF_INET)) == NULL) {
perror("gethostbyaddr error");
exit(1);
}
/* 打印相关信息 */
printf("official name of host : %s\n", host->h_name);
for (int i = 0; host->h_aliases[i]; i++) {
printf("alias list [%d] : %s\n", i, host->h_aliases[i]);
}
printf("host address type : %s\n", (host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
printf("length of address : %d\n", host->h_length);
for (int i = 0; host->h_addr_list[i]; i++) {
printf("list of addresses [%d], %s\n", i, inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
}
return 0;
}
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus/Documents/others# ./server 127.0.0.1
official name of host : localhost
host address type : AF_INET
length of address : 4
list of addresses [0], 127.0.0.1
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus/Documents/others#
十一、select(服务端)
select是最具代表性的实现I/O复用服务器端的方法,它的使用主要包括 select()
系统调用和四个宏函数。
1.设置文件描述符、指定监视范围、设置超时
利用 select()
函数可以同时监视多个文件描述符,因此首先需要将待监视的文件描述符集中到一起,也就是文件描述符集(file descriptor sets)中。
以下三个宏函数用于初始化文件描述符集并操作待监视的文件描述符:
FD_ZERO(fd_set *set)
:将参数set
指向的文件描述符集的所有位初始化为0。在select()
函数返回时,每个文件描述符集会被就地修改以指示哪些文件描述符准备就绪。因此如果在循环中使用select()
函数,每次调用前必须重新初始化集合。FD_SET(int fd, fd_set *set)
:在参数set
指向的文件描述符集中注册文件描述符fd
的信息。FD_CLR(int fd, fd_set *set)
:在参数set
指向的文件描述符集中清除文件描述符fd
的信息。
而 select()
函数的超时事件需要借助结构体timeval来实现:
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
2.调用 select 函数
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明:
nfds
:待监视的文件描述符数量。readfds
:对应fd_set关注文件描述符是否存在待读取数据,函数返回时会清除该fd_set中所有不存在读取事件的文件描述符。writefds
:对应fd_set关注文件描述符是否存在待写入数据,函数返回时会清除该fd_set中所有不存在写入事件的文件描述符。exceptfds
:对应fd_set关注文件描述符是否存在异常,函数返回时会清除该fd_set中所有不存在一场事件的文件描述符。timeout
:超时信息。一般情况下select()
函数只有在监视的文件描述符发生变化时才返回,但如果未发生变化,就会进入阻塞状态,指定超时时间就是为了防止这种情况的发生。
selelct()
函数用来验证上述三个监视项的变化情况,在发生错误时会返回-1,超时返回时返回0,因发生关注的事件时返回发生相应事件的文件描述符的数量。
3.查看调用结果
FD_ISSET(int fd, fd_set *set)
用于验证 select()
函数的调用结果,当参数 set
指向的变量中包含文件描述符 fd
的信息时返回真,这也表明对应的文件描述符发生了变化,或者说监视的文件描述符中发生了相应的监听事件。
4.通过 selelct 监视标准输入
#include <sys/select.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_SIZE 1024
int main() {
fd_set read_fd_set;
char buffer[BUF_SIZE];
struct timeval timeout;
while (1) {
/* 设置文件描述符、指定监视范围、设置超时 */
FD_ZERO(&read_fd_set);
FD_SET(0, &read_fd_set); // stdin的文件描述符为0
timeout.tv_sec = 5;
timeout.tv_usec = 0;
/* 调用select函数 */
int result = select(1, &read_fd_set, 0, 0, &timeout);
if (result == -1) {
perror("select error");
exit(1);
} else if (result == 0) {
printf("time-out\n");
} else {
if (FD_ISSET(0, &read_fd_set)) {
memset(buffer, 0, BUF_SIZE);
int len = read(0, buffer, BUF_SIZE);
printf("stdin : %s", buffer);
} else {
printf("other event\n");
}
}
}
}
atreus@MacBook-Pro % gcc code.c -o exe
atreus@MacBook-Pro % ./exe
a
stdin : a
b
stdin : b
c
stdin : c
time-out
time-out
time-out
^C
atreus@MacBook-Pro %
十二、epoll(服务端)
select虽然能够实现I/O复用,但它有以下两个不合理之处:
- 每次调用
select()
函数时,都需要向该函数传入监视的待读、待写与异常的文件描述符集,每次执行这个过程都需要与内核进行交互,这会造成额外的性能开销。 - 每次调用
select()
函数后,都需要对所有文件描述符进行一次遍历以检测文件描述符的变化,这在监听了大量文件描述符时也显得非常不必要。
而Linux从2.5.44版本开始引入的epoll则很好地解决了相应问题。epoll的使用主要包括 epoll_create()
、epoll_ctl()
和 epoll_wait()
。
1.epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
epoll_create()
系统调用用于创建监听文件描述符的保存空间,此保存空间由内核管理,称为epoll实例(epoll instance)。size
仅用于向操作系统建议epoll实例的大小,但实际上自Linux 2.6.8起,内核会完全忽略此参数。
在创建成功时会返回对应的epoll文件描述符,该文件描述符会用于所有后续的epoll调用。
2.epoll_ctl
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
epoll_ctl()
系统调用负责向epoll实例中注册待监视的文件描述符。
参数说明:
epfd
:epoll实例的文件描述符,也即epoll_create()
的返回值。op
:用于指定监视对象的添加EPOLL_CTL_ADD
、删除EPOLL_CTL_DEL
和修改EPOLL_CTL_MOD
操作。fd
:需要注册的监视对象的文件描述符。event
:需要注册的监视对象的事件类型。
其中epoll_event的监听事件成员epoll_data_t主要有以下选项:
EPOLLIN
:需要读取数据。EPOLLOUT
:输出缓冲为空,可以立即发送数据。EPOLLERR
:发生错误。EPOLLPRI
:收到带外(out-of-band,OOB)数据。EPOLLET
:以边缘触发的方式得到事件通知。
可以通过位或运算同时传递多个参数,其中 EPOLLIN
最为常用。
3.epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_wait()
系统调用用于等待事件的产生,类似于 select()
调用。
参数说明:
epfd
:epoll实例的文件描述符,也即epoll_create()
的返回值。events
:用于保存发生监听事件的文件描述符的集合。maxevents
:第二个参数中可以保存的最大事数。timeout
:等待时间,单位是毫秒,传递-1
表示一直等待直到有监听事件发生。
4.水平触发(LT)和边缘触发(ET)
epoll有水平触发(Level Trigger)和边缘触发(Edge Trigger)两种工作模式。
默认情况下,epoll采用水平触发模式工作,这时可以处理阻塞和非阻塞套接字。而通过以下代码可以将epoll改为边缘触发模式,但此时它只支持非阻塞套接字。
/* 为服务器套接字注册epoll监听 */
struct epoll_event event;
event.data.fd = m_server_sockfd;
event.events = EPOLLIN|EPOLLET; // 边缘触发监听写事件
if (epoll_ctl(server_epollfd, EPOLL_CTL_ADD, server_sockfd, &event) == -1) {
perror("epoll_ctl error");
exit(1);
}
水平触发模式与边缘触发模式的区别在于:
- 水平触发模式中,只要监听事件没有完全处理就会一直通知该事件。以
EPOLLIN
事件为例,只要接收方没有将输入缓冲中的数据全部取走,epoll就会一直通知此事件。因此水平触发代码较为简洁,但效率较低且难以实现数据接收和处理的分离。 - 边缘触发模式中,只要监听事件被处理过,不管是否处理完全,epoll都不会重复通知该事件,直到该文件描述符上再次发生相同的事件时为止。
附一、基于 TCP 的循环服务器模型
server.c(服务端):
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_MAX_SIZE 1024
int main(int argc, char *argv[]) {
int server_sockfd; // 服务器套接字描述符
char buffer[BUF_MAX_SIZE]; // 数据缓冲区
struct sockaddr_in server_addr; // 服务器套接字信息
/* 参数检查 */
if (argc != 2) {
printf("Command format : %s <port>!\n", argv[0]);
exit(1);
}
/* 获取服务器套接字文件描述符 */
if ((server_sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket error");
exit(1);
}
/* 初始化服务器套接字信息 */
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET; // 使用IPv4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 自动获取服务端IP地址
server_addr.sin_port = htons(atoi(argv[1])); // 对端口进行字节序转化
/* 因为最后需要服务器主动关闭连接,所以要设置服务器套接字为可重用 */
int option = 1;
if (setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)) == -1) {
perror("setsockopt error");
exit(1);
}
/* 绑定服务器套接字信息 */
if (bind(server_sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr_in)) < 0) {
perror("bind error");
exit(1);
}
/* 将服务器套接字转化为被动监听 */
if (listen(server_sockfd, 3) < 0) {
perror("listen error!");
exit(1);
}
/* 串行循环连接客户端 */
while (1) {
int client_sockfd; // 通信套接字描述符
struct sockaddr_in client_addr; // 客户端套接字信息
socklen_t len = sizeof(client_addr);
/* 等待客户端的连接 */
printf("[server] Server is waiting······\n");
memset(&client_addr, 0, sizeof(struct sockaddr_in));
if ((client_sockfd = accept(server_sockfd, (struct sockaddr *)(&client_addr), &len)) < 0) {
perror("accept error");
exit(1);
}
printf("[server] Client's port is %d, ip is %s.\n", ntohs(client_addr.sin_port), inet_ntoa(client_addr.sin_addr));
/* 接收客户端的消息后再发送给客户端 */
int msg_len = 0;
memset(buffer, 0, BUF_MAX_SIZE);
while ((msg_len = recv(client_sockfd, buffer, sizeof(buffer), 0)) != 0) {
printf("[server] Client's message : %s.\n", buffer);
send(client_sockfd, buffer, msg_len, 0);
}
/* 关闭连接 */
shutdown(client_sockfd, SHUT_RDWR);
}
/* 关闭连接 */
shutdown(server_sockfd, SHUT_RDWR);
return 0;
}
client.c(客户端):
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_MAX_SIZE 1024
int main(int argc, char *argv[]) {
int client_sockfd; // 客户端套接字描述符
char buffer[BUF_MAX_SIZE]; // 收发数据缓冲区
struct sockaddr_in server_addr; // 服务器套接字描述符
/* 参数检查 */
if (argc != 3) {
printf("Command format : %s <IP> <port>!\n", argv[0]);
exit(1);
}
/* 获取客户端套接字文件描述符 */
if ((client_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket error");
exit(1);
}
/* 初始化服务器套接字信息 */
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
server_addr.sin_port = htons(atoi(argv[2]));
/* 向服务器发送连接请求 */
if (connect(client_sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr_in)) < 0) {
perror("connect error");
exit(1);
}
printf("[client] Connect successfully.\n");
/* 向服务器发送消息 */
while (1) {
printf("[client] Input message (Q to quit) : ");
memset(buffer, 0, BUF_MAX_SIZE);
scanf("%[^\n]%*c", buffer);
if (!strcmp(buffer, "Q")) {
break;
}
send(client_sockfd, buffer, strlen(buffer), 0);
/* 接收服务端的消息 */
memset(buffer, 0, BUF_MAX_SIZE);
recv(client_sockfd, buffer, sizeof(buffer), 0);
printf("[client] Server's message : %s.\n", buffer);
}
/* 关闭连接 */
shutdown(client_sockfd, SHUT_RDWR);
return 0;
}
运行结果:
atreus@MacBook-Pro % gcc server.c -o server
atreus@MacBook-Pro % ./server 1521
[server] Server is waiting······
[server] Client's port is 59119, ip is 127.0.0.1.
[server] Client's message : a.
[server] Client's message : b.
[server] Client's message : c.
[server] Server is waiting······
^C
atreus@MacBook-Pro %
atreus@MacBook-Pro % gcc client.c -o client
atreus@MacBook-Pro % ./client 127.0.0.1 1521
[client] Connect successfully.
[client] Input message (Q to quit) : a
[client] Server's message : a.
[client] Input message (Q to quit) : b
[client] Server's message : b.
[client] Input message (Q to quit) : c
[client] Server's message : c.
[client] Input message (Q to quit) : Q
atreus@MacBook-Pro %
atreus@MacBook-Pro % lsof -Pni :1521
atreus@MacBook-Pro % lsof -Pni :1521
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 1887 atreus 3u IPv4 0x90048f15a6bc2705 0t0 TCP *:1521 (LISTEN)
atreus@MacBook-Pro % lsof -Pni :1521
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 1887 atreus 3u IPv4 0x90048f15a6bc2705 0t0 TCP *:1521 (LISTEN)
server 1887 atreus 4u IPv4 0x90048f15a73ae705 0t0 TCP 127.0.0.1:1521->127.0.0.1:59119 (ESTABLISHED)
client 2019 atreus 3u IPv4 0x90048f15a6b3a965 0t0 TCP 127.0.0.1:59119->127.0.0.1:1521 (ESTABLISHED)
atreus@MacBook-Pro %
附二、基于 UDP 的循环服务器模型
server.c(服务端):
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_MAX_SIZE 1024
int main(int argc, char *argv[]) {
int server_sockfd; // 服务器套接字描述符
char buffer[BUF_MAX_SIZE]; // 数据缓冲区
struct sockaddr_in server_addr; // 服务器套接字信息
/* 参数检查 */
if (argc != 2) {
printf("Command format : %s <port>!\n", argv[0]);
exit(1);
}
/* 获取服务器套接字文件描述符 */
if ((server_sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket error");
exit(1);
}
/* 初始化服务器套接字信息 */
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET; // 使用IPv4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 自动获取服务端IP地址
server_addr.sin_port = htons(atoi(argv[1])); // 对端口进行字节序转化
/* 绑定服务器套接字信息 */
if (bind(server_sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr_in)) < 0) {
perror("bind error");
exit(1);
}
/* 串行循环连接客户端 */
while (1) {
struct sockaddr_in client_addr; // 服务器套接字信息
socklen_t len = sizeof(client_addr);
/* 等待客户端的连接 */
printf("[server] Server is waiting······\n");
memset(&client_addr, 0, sizeof(struct sockaddr_in));
/* 接收客户端的消息后再发送给客户端 */
int msg_len = 0;
memset(buffer, 0, BUF_MAX_SIZE);
msg_len = recvfrom(server_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)(&client_addr), &len);
printf("[server] Client's port is %d, ip is %s.\n", ntohs(client_addr.sin_port), inet_ntoa(client_addr.sin_addr));
printf("[server] Client's message : %s.\n", buffer);
sendto(server_sockfd, buffer, msg_len, 0, (struct sockaddr *)(&client_addr), len);
}
return 0;
}
client.c(客户端):
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_MAX_SIZE 1024
int main(int argc, char *argv[]) {
int client_sockfd; // 客户端套接字描述符
char buffer[BUF_MAX_SIZE]; // 收发数据缓冲区
struct sockaddr_in server_addr; // 服务器套接字描述符
/* 参数检查 */
if (argc != 3) {
printf("Command format : %s <IP> <port>!\n", argv[0]);
exit(1);
}
/* 获取客户端套接字文件描述符 */
if ((client_sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket error");
exit(1);
}
/* 初始化服务器套接字信息 */
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
server_addr.sin_port = htons(atoi(argv[2]));
/* 向服务器发送消息 */
while (1) {
socklen_t len = sizeof(server_addr);
printf("[client] Input message (Q to quit) : ");
memset(buffer, 0, BUF_MAX_SIZE);
scanf("%[^\n]%*c", buffer);
if (!strcmp(buffer, "Q")) {
break;
}
sendto(client_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)(&server_addr), len);
/* 接收服务端的消息 */
memset(buffer, 0, BUF_MAX_SIZE);
recvfrom(client_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)(&server_addr), &len);
printf("[client] Server's message : %s.\n", buffer);
}
/* 关闭连接 */
shutdown(client_sockfd, SHUT_RDWR);
return 0;
}
运行结果:
atreus@MacBook-Pro % gcc server.c -o server
atreus@MacBook-Pro % ./server 1521
[server] Server is waiting······
[server] Client's port is 54097, ip is 127.0.0.1.
[server] Client's message : a.
[server] Server is waiting······
[server] Client's port is 54097, ip is 127.0.0.1.
[server] Client's message : b.
[server] Server is waiting······
[server] Client's port is 54097, ip is 127.0.0.1.
[server] Client's message : c.
[server] Server is waiting······
^C
atreus@MacBook-Pro %
atreus@MacBook-Pro % gcc client.c -o client
atreus@MacBook-Pro % ./client 127.0.0.1 1521
[client] Input message (Q to quit) : a
[client] Server's message : a.
[client] Input message (Q to quit) : b
[client] Server's message : b.
[client] Input message (Q to quit) : c
[client] Server's message : c.
[client] Input message (Q to quit) : Q
atreus@MacBook-Pro %
atreus@MacBook-Pro % lsof -Pni :1521
atreus@MacBook-Pro % lsof -Pni :1521
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 67822 atreus 3u IPv4 0x90048f0275f6ddad 0t0 UDP *:1521
atreus@MacBook-Pro % lsof -Pni :54097
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
client 67886 atreus 3u IPv4 0x90048f0275f6376d 0t0 UDP *:54097
atreus@MacBook-Pro %
附三、基于 TCP 和 epoll 的循环服务器模型
server.c(服务端):
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_MAX_SIZE 1024
#define TASK_MAX_NUM 10
int main(int argc, char *argv[]) {
int server_sockfd; // 服务器套接字描述符
int server_epollfd; // 服务器epoll文件描述符
char buffer[BUF_MAX_SIZE]; // 数据缓冲区
struct sockaddr_in server_addr; // 服务器套接字信息
struct epoll_event event_arr[TASK_MAX_NUM]; // 用于存储监听到指定事件的epoll实例
struct epoll_event event; // epoll监听事件
/* 参数检查 */
if (argc != 2) {
printf("Command format : %s <port>!\n", argv[0]);
exit(1);
}
/* 获取服务器套接字文件描述符 */
if ((server_sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket error");
exit(1);
}
/* 初始化服务器套接字信息 */
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET; // 使用IPv4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 自动获取服务端IP地址
server_addr.sin_port = htons(atoi(argv[1])); // 对端口进行字节序转化
/* 因为最后需要服务器主动关闭连接,所以要设置服务器套接字为可重用 */
int option = 1;
if (setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)) == -1) {
perror("setsockopt error");
exit(1);
}
/* 绑定服务器套接字信息 */
if (bind(server_sockfd, (struct sockaddr *) (&server_addr), sizeof(struct sockaddr_in)) < 0) {
perror("bind error");
exit(1);
}
/* 将服务器套接字转化为被动监听 */
if (listen(server_sockfd, 3) < 0) {
perror("listen error!");
exit(1);
}
/* 创建epoll句柄 */
if ((server_epollfd = epoll_create(10)) == -1) {
perror("epoll_create error");
exit(1);
}
/* 为服务器套接字注册epoll监听 */
event.data.fd = server_sockfd;
event.events = EPOLLIN; // 水平触发监听写事件
if (epoll_ctl(server_epollfd, EPOLL_CTL_ADD, server_sockfd, &event) == -1) {
perror("epoll_ctl error");
exit(1);
}
/* 串行循环连接客户端 */
while (1) {
int ready_num = epoll_wait(server_epollfd, event_arr, TASK_MAX_NUM, -1);
for (int i = 0; i < ready_num; i++) {
if (event_arr[i].events == EPOLLIN) { /* 对应文件描述符可读 */
printf("[server] Client %d has a request.\n", event_arr[i].data.fd);
if (event_arr[i].data.fd == server_sockfd) { /* 客户端发来连接请求 */
/* 连接客户端 */
int client_sockfd; // 通信套接字描述符
struct sockaddr_in client_addr; // 客户端套接字信息
socklen_t len = sizeof(client_addr);
/* 等待客户端的连接 */
printf("[server] Server is waiting······\n");
memset(&client_addr, 0, sizeof(struct sockaddr_in));
if ((client_sockfd = accept(server_sockfd, (struct sockaddr *) (&client_addr), &len)) < 0) {
perror("accept error");
exit(1);
}
printf("[server] Client's port is %d, ip is %s.\n", ntohs(client_addr.sin_port), inet_ntoa(client_addr.sin_addr));
/* 为新连接的客户端套接字注册监听 */
event.data.fd = client_sockfd;
event.events = EPOLLIN; // 水平触发监听写事件
if (epoll_ctl(server_epollfd, EPOLL_CTL_ADD, client_sockfd, &event) == -1) {
perror("epoll_ctl error");
exit(1);
}
} else { /* 客户端发来事务请求 */
/* 接收客户端的消息后再发送给客户端 */
int msg_len = 0;
memset(buffer, 0, BUF_MAX_SIZE);
while ((msg_len = recv(event_arr[i].data.fd, buffer, sizeof(buffer), 0)) != 0) {
printf("[server] Client's message : %s.\n", buffer);
send(event_arr[i].data.fd, buffer, msg_len, 0);
}
/* 关闭连接 */
shutdown(event_arr[i].data.fd, SHUT_RDWR);
/* 从epoll中删除对应的文件描述符监听 */
if (epoll_ctl(server_epollfd, EPOLL_CTL_DEL, event_arr[i].data.fd, &event) == -1) {
perror("[server] epoll_ctl error");
} else {
printf("[server] Epoll delete successfully.\n");
}
}
} else {
printf("[server] Other events.\n");
}
}
}
/* 关闭连接 */
shutdown(server_sockfd, SHUT_RDWR);
return 0;
}
client.c(客户端):
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_MAX_SIZE 1024
int main(int argc, char *argv[]) {
int client_sockfd; // 客户端套接字描述符
char buffer[BUF_MAX_SIZE]; // 收发数据缓冲区
struct sockaddr_in server_addr; // 服务器套接字描述符
/* 参数检查 */
if (argc != 3) {
printf("Command format : %s <IP> <port>!\n", argv[0]);
exit(1);
}
/* 获取客户端套接字文件描述符 */
if ((client_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket error");
exit(1);
}
/* 初始化服务器套接字信息 */
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
server_addr.sin_port = htons(atoi(argv[2]));
/* 向服务器发送连接请求 */
if (connect(client_sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr_in)) < 0) {
perror("connect error");
exit(1);
}
printf("[client] Connect successfully.\n");
/* 向服务器发送消息 */
while (1) {
printf("[client] Input message (Q to quit) : ");
memset(buffer, 0, BUF_MAX_SIZE);
scanf("%[^\n]%*c", buffer);
if (!strcmp(buffer, "Q")) {
break;
}
send(client_sockfd, buffer, strlen(buffer), 0);
/* 接收服务端的消息 */
memset(buffer, 0, BUF_MAX_SIZE);
recv(client_sockfd, buffer, sizeof(buffer), 0);
printf("[client] Server's message : %s.\n", buffer);
}
/* 关闭连接 */
shutdown(client_sockfd, SHUT_RDWR);
return 0;
}
运行结果:
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus/Documents/others# gcc server.c -o server
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus/Documents/others# ./server 4399
[server] Client 3 has a request.
[server] Server is waiting······
[server] Client's port is 44446, ip is 127.0.0.1.
[server] Client 5 has a request.
[server] Client's message : a.
[server] Client's message : b.
[server] Client's message : c.
[server] Epoll delete successfully.
[server] Client 3 has a request.
[server] Server is waiting······
[server] Client's port is 44452, ip is 127.0.0.1.
[server] Client 6 has a request.
[server] Client's message : 1.
[server] Client's message : 2.
[server] Client's message : 3.
[server] Epoll delete successfully.
^C
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus/Documents/others#
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus/Documents/others# gcc client.c -o client
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus/Documents/others# ./client 127.0.0.1 4399
[client] Connect successfully.
[client] Input message (Q to quit) : a
[client] Server's message : a.
[client] Input message (Q to quit) : b
[client] Server's message : b.
[client] Input message (Q to quit) : c
[client] Server's message : c.
[client] Input message (Q to quit) : Q
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus/Documents/others# ./client 127.0.0.1 4399
[client] Connect successfully.
[client] Input message (Q to quit) : 1
[client] Server's message : 1.
[client] Input message (Q to quit) : 2
[client] Server's message : 2.
[client] Input message (Q to quit) : 3
[client] Server's message : 3.
[client] Input message (Q to quit) : ^C
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus/Documents/others#
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus# lsof -Pni :4399
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus# lsof -Pni :4399
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 274436 root 3u IPv4 2951291 0t0 TCP *:4399 (LISTEN)
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus# lsof -Pni :4399
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 274436 root 3u IPv4 2951291 0t0 TCP *:4399 (LISTEN)
server 274436 root 5u IPv4 2951569 0t0 TCP 127.0.0.1:4399->127.0.0.1:44446 (ESTABLISHED)
client 274513 root 3u IPv4 2951568 0t0 TCP 127.0.0.1:44446->127.0.0.1:4399 (ESTABLISHED)
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus# lsof -Pni :4399
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 274436 root 3u IPv4 2951291 0t0 TCP *:4399 (LISTEN)
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus# lsof -Pni :4399
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 274436 root 3u IPv4 2951291 0t0 TCP *:4399 (LISTEN)
server 274436 root 6u IPv4 2952249 0t0 TCP 127.0.0.1:4399->127.0.0.1:44452 (ESTABLISHED)
client 274687 root 3u IPv4 2952248 0t0 TCP 127.0.0.1:44452->127.0.0.1:4399 (ESTABLISHED)
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus# lsof -Pni :4399
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 274436 root 3u IPv4 2951291 0t0 TCP *:4399 (LISTEN)
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus# lsof -Pni :4399
root@iZwz9fsfltolu74amg1v0rZ:/home/atreus#
更多推荐
所有评论(0)