2024-06-03
大学课程
00

目录

1 组播
2 三元组、五元组
3 netstat命令相关
4 常用端口
5 大端序、小端序
6 网络编程常识(大概?)
6.1 C/S模型
7 c语言下多线程API,同步问题
8 编程模式相关,非阻塞模式的两种模型
8.1 select模型
8.2 epoll模型
9 Linux下 socket相关函数及参数
10 两种非阻塞模式服务端的代码示例
10.1 select模型
10.2 epoll模型

2024年6月4号,网络编程技术课程期末复习

题型:

  1. 单选题20个,40分
  2. 判断题15个,15分
  3. 简答题4个,29分
  4. 程序设计,16分

选择题知识点:

基本概念;网络编程常识;基本函数使用;三元组和五元组;字节转换;熟知端口;多线程有关的理论;接收数据的处理流式套接字;windows编程三个题,有有关图形界面的;多线程同步问题;阻塞非阻塞;组播;非阻塞模式下错误处理相关内容;异步选择模型;netstat命令。

简答题知识点:

绪论一道8分;绪论幻灯片有一道4分,跟乱码有关系;所有编程模式总结,单线程、多线程等,考的与并发性有关;第四个是程序填空写代码

最后一道大题程序设计非阻塞模式服务端,16分

1 组播

组播和广播一样,都是对范围内的一定设备进行报文传输的一种方式。组播是主机间一对多的通讯模式, 组播是一种允许一个或多个组播源发送同一报文到多个接收者的技术。和对多个主机进行单播,数据源要同时发出多份报文不同,对多个主机进行组播时,数据源实际仅需要发出一份报文。并且,组播可以跨网段传输。

IPv4组播使用D类地址空间,所有地址的最高位固定为1110,所以实际组播可以使用的地址范围为224.0.1.0到239.255.255.255,其中224.0.0.x保留为路由协议等使用。

2 三元组、五元组

三元组:ip地址,端口,三层协议,用于标识一个网络上的一个主机的进程

五元组:源ip地址,目标ip地址,源端口,目标端口,四层协议,用于标识网络上两个主机的一个会话

3 netstat命令相关

netstat命令用于显示Linux系统的整个网络状态,它的语法如下:

netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip]

它的可选参数如下:

  • -a或--all 用于显示所有连线中的socket
  • -A <网络类型>或-A --<网络类型> 用于列出该网络类型连线中的所有地址。
  • -c或--continuous 用于持续列出网络状态。
  • -e或--extend 用于显示网络其他相关信息。
  • -g或--groups 用于显示组播组员名单。
  • -i或--interfaces 用于显示网络设备(网卡,环回接口等)的状态。
  • -l或--listening 用于显示处于Listen的socket
  • -n或--numeric 用于直接显示ip而不显示域名
  • -N或--netlink或--symbolic 用于显示网络硬件外围设备的套接字名称。
  • -o或--timers 用于在网络连接中多显示一列计时器。
  • -p或--programs 用于显示正在使用socket的程序pid和程序名称
  • -r或--route 用于显示本机路由表。
  • -s或--statistics 用于显示网络工作信息统计。
  • -t或--tcp 显示tcp协议下的连接情况
  • -u或--udp 显示udp协议下的连接情况
  • --ip或--inet 和-A inet参数等效。
  • -x或--unix 和-A unix参数等效。

提示

注意参数只有一个-a的话,netstat不会显示Listen状态的连接的,必须使用-l

个人比较常用的一个netstat格式是这样的:netstat -tunlp | grep ':80',显示tcp和udp下的所有连接,直接显示ip,并且显示连接的程序的信息,另外还对':80'这个字符串进行查找,用于查找是否有程序占用了80端口。

image.png

4 常用端口

以下为IANA规定的,一些为应用层协议而保留的常用端口。这些仅为推荐使用,不代表实际使用中一定要用这些端口。反过来说,这些0到1024的常用端口也极其不推荐给别的程序使用。

  • 0,tcp/udp,保留端口,默认不使用。如果发送过程中不准备接受回复信息,那么可以将其作为源端口。
  • 1,tcp/udp,TCPMUX(TCP端口服务多路开关选择器)使用。
  • 7,tcp/udp,Echo回显使用,对,就是那个命令行的echo。
  • 20、21,tcp/udp,FTP使用。
  • 22,tcp/udp,SSH使用。
  • 23,tcp/udp,Telnet终端仿真协议使用。
  • 25,tcp/udp,SMTP使用。
  • 43,tcp,WHOIS协议使用。
  • 53,tcp/udp,DNS使用。
  • 80,tcp/udp,如果是tcp,那就是HTTP;如果是udp,那就是QUIC协议。
  • 107,tcp,远程Telnet协议。
  • 110,tcp,未加密的POP3。
  • 179,tcp,BGP使用。
  • 443,tcp,HTTPS或QUIC协议,同80端口。
  • 989、990,tcp/udp,加了TLS/SSL的FTP。

老师应该不考1024以上的常用端口吧(?)

提示

QUIC协议,由Google的一名员工设计,以udp协议为基础,试图改进HTTP协议中速度不够快并且一旦出错就会阻塞发送过程的问题。该协议在2018年10月在HTTP3中正式替代TCP,并且2021年5月推出标准化版本。

5 大端序、小端序

在所有的平台上,多字节对象都会被存储为连续的字节。连续就会有顺序,就有了大端序和小端序这两种顺序差别。

  • 大端序:将数据的低位字节放在内存的高位地址,高位字节放在内存的低位地址。如果要可视化的话,假设内存是从左向右地址逐渐增加,那么大端序即为人类阅读顺序。
  • 小端序:与大端序完全相反,将数据的高位字节放在内存的低位地址,低位字节放在内存的高位地址。小端序和人类阅读顺序相反,但是更符合计算机的读取顺序,因为读内存通常都是从低位向高位读取。

因为小端序更符合计算机的读取顺序,所以采用小端序的计算机一般比大端序更快,所以如今的计算机大多采用小端序。但是网络传输或者文件存储,一般会为人类阅读的,并且不影响处理效率的,则采用小端序。

关于大端序(网络序)和小端序(主机序)转换,需要四个函数:

  • htonl:将无符号32位整型从主机序转换为网络序
  • htons:将无符号16位整型从主机序转换为网络序
  • ntohl:将无符号32位整型从网络序转换为主机序
  • ntohs:将无符号16为整型从网络序转换为主机序

示例:

cpp
int port = atoi(argv[2]); // 获取运行参数port addr.sin_port = htons(port); // 转换网络序

6 网络编程常识(大概?)

这个题实在太宽泛了,随便写点吧。

计算机的发展史,就是对已经有的各类技术进行封装和抽象,并在此基础上发展新的技术的过程。网络通信也是如此,一次封装和抽象就是一个技术栈,栈与栈的不停叠加,最终就形成了今天发达的互联网。

按照五层模型来说的话:

  • 物理层:处理具体的信号,确保二进制数据和物理信号之间的转换,向上层屏蔽了数据通信相关的细节,比如什么调制器解调器一类的。
  • 数据链路层:确保在同一网络下两个网络主机的数据链路的建立和管理,它定义了互联网络的两个设备之间传输数据的规范,向上层屏蔽了发送的细节,只要网络层将数据和目的交给它,它确定能够送到。
  • 网络层:提供路由和寻址功能,使两个可能处于不同子网的主机也能够互相顺利地通信,向上层屏蔽了发送过程中的路由细节,只要传输层将数据和目的交给它,它就能以最佳路径将数据送给对方。
  • 传输层:提供面向连接的数据流支持,可靠性,流量控制,多路复用等服务,使两个不知道在哪的但是能够互相通信的两个主机能够同时互相发送大量消息,向上层屏蔽了什么拥塞控制,什么自动重传之类的细节,上层只要把数据交给它,它能够确保对方收到的还是这些数据。
  • 应用层:直接面向需求的层,在传输层的基础上,定义每次发送的消息的具体的含义,并为用户提供服务。

网络编程,可能涉及到的方向有网络层(网络安全,时延控制,可靠传输等方向,或者是对已有的传输层两大协议进行改进)、传输层(制订应用层协议方向,比如上面提到过的QUIC)和应用层(应用层协议的二次开发方向,比如基于HTTP的RPC服务)

6.1 C/S模型

Client/Server模型(简称C/S模型)是最常见的通信模型,由一个Server/Server集群为任意的Client提供服务。

Server的特性包括被动等待客户端的连接,连续、长期、稳定运行,一般需要较多资源和稳定的操作系统支持,可同时服务多个客户端等。Client的特性包括主动发起连接请求,可以访问多种服务端,对硬件和操作系统要求不高等。

客户端和服务端本质上都是具有通信功能的进程,一个计算机极有可能同时运行多种客户端和服务端,同时一个进程有可能既是客户端又是服务端。

不同的操作系统通信要考虑的问题包括:差错控制与处理,字节序,字长,字节定界问题和字符编码,时间表示等等。

7 c语言下多线程API,同步问题

  1. int pthread_create(pthread_t *restrict threadID, const pthread_attr_t *restrict attr, void * ( *start_routine)(void*), void * restrict arg),创建线程,start_routine即为创建的新的线程的执行函数。
  2. int pthread_join(pthread_t threadID, void **status),等待线程,阻塞当前线程,直至threadID所对应的线程执行结束并释放资源。
  3. int pthread_detach(pthread_t threadID),分离线程,threadID对应的线程会在执行结束后自动释放资源,并且不会阻塞调用方线程。
  4. void pthread_exit(void *retval),结束调用方所在线程。注意主线程调用return会导致进程的结束,所有线程不管执行是否结束都会一并结束;调用pthread_exit则不会强行结束其他线程。

进程是操作系统用于管理运行过程中的程序实例的基本单位,由代码,数据和进程控制块PCB组成。线程是缩小版本的进程,它是CPU调度的基本单位,共享进程的资源,有自己独立的栈和寄存器,每个进程至少包括一个主线程,主线程执行完毕后进程也将结束。

线程的运行是异步的,各自运行各自的,线程间通信一般有这么几个方法:

  • 互斥锁、读写锁、条件变量
  • 信号量
  • 类似go中的channel等不同语言各自实现的方式

以c为例,线程间通信的方式可以有:

  1. 由互斥锁/读写锁/条件变量保护的共享内存,两个线程读写同一变量。
  2. 由互斥锁+队列组成的消息队列,匹配消费者-生产者问题。
  3. 信号量counting_semaphore对象,在适当的位置加减即可实现线程的同步。(在别的语言里,这东西也能叫waitgroup一类的,调用方式也可能是+1而不是-1)

c语言下的互斥锁:

  1. pthread_mutex_init(pthread_mutex_t*, const pthread_mutexattr_t*),创建锁。
  2. pthread_mutex_lock(pthread_mutex_t*),加锁。
  3. pthread_mutex_unlock(pthread_mutex_t*),解锁。
  4. pthread_mutex_destroy(pthread_mutex_t*),销毁锁。

c语言下的信号量:

  1. int sem_init(sem_t *sem, int pshared, int value),创建信号量并且指定信号量初始值。如果该信号量仅在当前进程下使用,那么pshared参数指定为0。
  2. int sem_wait(sem_t *sem),信号量-1,如果不能减1那么会阻塞线程直至在别的线程中信号量+1为止。
  3. int sem_post(sem_t *sem),信号量+1。
  4. int sem_destroy(sem_t *sem),销毁信号量。

8 编程模式相关,非阻塞模式的两种模型

8.1 select模型

select模型会将程序所创建的所有套接字存入三个集合之后,交给操作系统进行管理,如果这些套接字中有产生了新的连接/收到了新的消息,那么select会返回发生了变化的套接字的个数,由程序自己自行检查哪些套接字发生变化。

select模型使用(注意,fd_set是专为文件描述符准备的集合类型):

  1. 使用FD_ZERO(fd_set* set)函数,将三个集合(可读性集合,可写性集合,错误集合)清零。
  2. 使用FD_SET(int fd, fd_set* set)函数,将套接字按需加入集合。
  3. 调用select函数。

提示

select函数的五个参数分别是:

  • int maxfd,要select的套接字数量
  • fd_set *readfds,可读性集合
  • fd_set *writefds,可写性集合
  • fd_set *exceptfds,错误/意外集合
  • const struct timeval *timeout,要等待的时间,select会阻塞线程timeout这么长时间。如果这里传入NULL,则select会一直阻塞线程,直到所有集合上有套接字符合条件;如果传入0,则会立即返回。

注意,Windows和Linux的select参数有一些不同。Windows端的maxfd参数是无意义的,默认为0,而Linux的maxfd参数是有实际意义的;Windows的timeout仅需设置一次,而Linux的timeout必须每次都设置(这个“每次都设置”,是因为select会修改这个timeout)。除此之外,Windows的这个fd_set大小默认为64,如果要修改大小,要在include <winsock2.h>前定义;而Linux默认1024,可以任意修改大小。

  1. 调用结束后,对三个集合的所有套接字,调用FD_ISSET(int fd, fd_set* set)函数判断是否还在集合中。如果还在,则进行处理,该读的读,该写的写。

提示

select的返回值可以是0(时间截止,仍没有一个套接字符合要求),-1(发生错误)或者是其他的值,表示状态变化符合条件的套接字数量。

select的缺点在于,每次调用select,这些套接字集合都有从用户态向内核态,再从内核态向用户态的拷贝过程;另外,select会对所有套接字进行遍历,select结束后程序也大概率会对所有套接字进行遍历,这在套接字过多时耗时容易过长。

8.2 epoll模型

epoll模型在select模型上更进一步,不但返回发生了变化的套接字个数,还会将哪些套接字发生了什么事件一并返回,程序就不需要再对所有套接字进行检查,仅需要对发生的时间和套接字进行处理即可。

epoll没学Winsock相关。

epoll模型使用:

  1. 通过create_epoll(int maxfds)创建一个epoll的句柄,其中maxfds为epoll所支持的最大套接字数量(新版本后取消该参数)。
  2. 通过int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);来将套接字添加进epoll。op类似select的那三个集合条件,分别为EPOLL_CTL_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD。
  3. 创建一个epoll_event的数组,数组长度等于create_epoll(int maxfds)中传入的数字。然后循环调用epoll_wait(int epfd, strcuct epoll_event* events, int maxevents, int timeout),调用结束后,遍历epoll_event,通过数组中的事件来处理对应套接字,其形式大概类似这样:
c
struct epoll_event events[MAX_EVENTS]; // 事件数组 int numEvents = epoll_wait(epollfd, events, MAX_EVENTS, timeout); for (int i = 0; i < numEvents; i++) { int sockfd = events[i].data.fd; if (events[i].events & EPOLLIN) { // sockfd 就绪,可读事件发生 // 处理可读事件的逻辑... } if (events[i].events & EPOLLOUT) { // sockfd 就绪,可写事件发生 // 处理可写事件的逻辑... } // 处理其他事件类型... }

epoll模型的两种工作模式:

  • 水平触发/状态触发:当套接字满足条件时,每次调用epoll_wait都会返回该套接字对应的事件,直到程序处理该事件。
  • 边缘触发:当套接字满足条件时,每次调用epoll_wait只会返回一次该套接字对应的事件。即使程序不处理它,下次epoll它仍然满足条件,该套接字对应事件也不会出现在epoll_event数组中了。

epoll事件有以下类型:

  1. EPOLLIN:表示可读事件,当文件描述符上有数据可读时触发。
  2. EPOLLOUT:表示可写事件,当文件描述符可写入数据时触发。
  3. EPOLLPRI:表示紧急事件,当文件描述符上有紧急数据可读时触发。
  4. EPOLLERR:表示错误事件,当文件描述符发生错误时触发。
  5. EPOLLHUP:表示挂起事件,当文件描述符挂起(例如对端关闭连接)时触发。
  6. EPOLLET:表示边缘触发模式,使用该模式可以实现高效的事件驱动。
  7. EPOLLONESHOT:表示单次触发模式,当事件发生后,文件描述符将被从 epoll 集合中移除,需要重新添加才能继续监听该文件描述符。

9 Linux下 socket相关函数及参数

  1. int socket(int domain, int type, int protocol),创建套接字,domain用于指定套接字的通信域,一般情况下直接用AF_INET即可;type用于指定套接字类型,常用的为SOCK_DGRAM(UDP),DOCK_STREAM(TCP);protocol一般为0。
  2. inet_addr([]char),用于转换字符串ip到in_addr_t类型。
  3. int bind(int sockfd, struct sockaddr *local_addr, int addrlen),绑定ip。
  4. int connect(int sockfd, struct sockaddr *peer_addr, int addrlen),连接远程套接字。
  5. int listen(int sockfd, int backlog),监听,tcp专属;backlog参数指定请求队列长度,如果它满,则客户端想要发起连接时会报错ECONNREFUSED
  6. int accept(int sockfd, void *addr, int *addrlen),接收连接,并且返回用于和客户端通信的套接字。
  7. int send(int sockfd, constant void *msg, int len, int flags),发送数据,flags一般为0。该函数仅将数据从用户态复制到内核态,何时发送由操作系统决定。
  8. int write(int sockfd, constant void *msg, int len),当sockfd为套接字时,功能和send一致。
  9. int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int *tolen),指定地址发送,udp专属版send。它和send都是如果发送成功,就返回实际发送的字节数,反之则返回-1。
  10. int recv(int sockfd, const void *buf, int len, int flags),接收数据,并将数据写入buf数组。
  11. int read(int sockfd, const void *buf, int len),读取数据,比较类似write和send的关系。
  12. int recvfrom(int sockfd, const void *buf, int len, unsigned int flags, const struct sockaddr *from, int *fromlen),类似sendto和send的关系,但是udp在一些情况下也能使用recv和send。
  13. int setsockopt(int sockfd, int level, int name, char *value, int *optlen)int getsockopt(int sockfd, int level, int name, char *value, int *optlen),一个设置套接字选项,一个获取套接字设置,主要用来设置广播/多播。
  14. int ioctl(int fd, unsigned long request,…),设置文件描述符选项,可以设置套接字的阻塞/非阻塞模式。
  15. int close(int fd),关闭套接字。
  16. int shutdown(int fd, int how),关闭数据传输,它应先于close使用,但是不调用它直接close一般也不会出问题。
  17. int getpeername(int sockfd, struct sockaddr *addr, int *addrlen),获取套接字对端信息。
  18. int gethostname(char *hostname, size_t size),获取主机名。

10 两种非阻塞模式服务端的代码示例

10.1 select模型

c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #define MAX_CLIENTS 10 #define BUFFER_SIZE 1024 int main() { int server_fd, max_fd, activity, client_socket[MAX_CLIENTS], i, valread, sd; int max_sd; fd_set readfds; char buffer[BUFFER_SIZE]; struct sockaddr_in server_addr; // 创建服务器套接字 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket 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)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 监听连接 if (listen(server_fd, 3) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } printf("Waiting for connections...\n"); // 初始化客户端套接字数组 for (i = 0; i < MAX_CLIENTS; i++) { client_socket[i] = 0; } while (1) { // 清空文件描述符集合 FD_ZERO(&readfds); // 将服务器套接字添加到集合 FD_SET(server_fd, &readfds); max_sd = server_fd; // 将客户端套接字添加到集合 for (i = 0; i < MAX_CLIENTS; i++) { sd = client_socket[i]; // 如果套接字有效,将其添加到集合 if (sd > 0) { FD_SET(sd, &readfds); } // 更新最大文件描述符 if (sd > max_sd) { max_sd = sd; } } // 使用 select 监听活动套接字 activity = select(max_sd + 1, &readfds, NULL, NULL, NULL); if ((activity < 0) && (errno != EINTR)) { perror("select failed"); } // 如果服务器套接字有活动,表示有新连接 if (FD_ISSET(server_fd, &readfds)) { int new_socket; struct sockaddr_in client_addr; int addrlen = sizeof(client_addr); // 接受新连接 if ((new_socket = accept(server_fd, (struct sockaddr *)&client_addr, (socklen_t *)&addrlen)) < 0) { perror("accept failed"); exit(EXIT_FAILURE); } // 打印客户端的主机名和地址 char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN); printf("New connection: %s:%d\n", client_ip, ntohs(client_addr.sin_port)); // 将新连接添加到客户端套接字数组 for (i = 0; i < MAX_CLIENTS; i++) { if (client_socket[i] == 0) { client_socket[i] = new_socket; break; } } } // 检查客户端套接字的活动 for (i = 0; i < MAX_CLIENTS; i++) { sd = client_socket[i]; if (FD_ISSET(sd, &readfds)) { // 读取客户端发送的消息 if ((valread = read(sd, buffer, BUFFER_SIZE)) == 0) { // 客户端断开连接 getpeername(sd, (struct sockaddr *)&server_addr, (socklen_t *)&addrlen); printf("Client disconnected: %s:%d\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port)); close(sd); client_socket[i] = 0; } else { // 打印客户端的主机名和消息 buffer[valread] = '\0'; printf("From %s:%d: %s\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port), buffer); } } } } return 0; }

10.2 epoll模型

c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/epoll.h> #include <sys/socket.h> #include <arpa/inet.h> #define MAX_EVENTS 10 #define BUFFER_SIZE 1024 int main() { int server_fd, epoll_fd, event_count, i, client_fd; struct epoll_event event, events[MAX_EVENTS]; char buffer[BUFFER_SIZE]; struct sockaddr_in server_addr, client_addr; socklen_t addrlen = sizeof(client_addr); // 创建服务器套接字 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket 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)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 监听连接 if (listen(server_fd, 3) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } printf("Waiting for connections...\n"); // 创建 epoll 实例 if ((epoll_fd = epoll_create1(0)) < 0) { perror("epoll_create1 failed"); exit(EXIT_FAILURE); } // 将服务器套接字添加到 epoll 实例的事件集合中 event.events = EPOLLIN; event.data.fd = server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) { perror("epoll_ctl failed"); exit(EXIT_FAILURE); } while (1) { // 等待事件发生 event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (event_count < 0) { perror("epoll_wait failed"); exit(EXIT_FAILURE); } // 处理所有发生的事件 for (i = 0; i < event_count; i++) { // 如果是服务器套接字上的事件,表示有新连接 if (events[i].data.fd == server_fd) { // 接受新连接 if ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addrlen)) < 0) { perror("accept failed"); exit(EXIT_FAILURE); } // 打印客户端的主机名和地址 char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN); printf("New connection: %s:%d\n", client_ip, ntohs(client_addr.sin_port)); // 将新连接添加到 epoll 实例的事件集合中 event.events = EPOLLIN; event.data.fd = client_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) < 0) { perror("epoll_ctl failed"); exit(EXIT_FAILURE); } } // 如果是客户端套接字上的事件,表示有数据到达 else { client_fd = events[i].data.fd; // 读取客户端发送的消息 int valread = read(client_fd, buffer, BUFFER_SIZE); if (valread <= 0) { // 客户端断开连接 getpeername(client_fd, (struct sockaddr *)&client_addr, &addrlen); char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN); printf("Client disconnected: %s:%d\n", client_ip, ntohs(client_addr.sin_port)); // 从 epoll 实例的事件集合中移除客户端套接字 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL); close(client_fd); } else { // 打印客户端的主机名和消息 buffer[valread] = '\0'; char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN); printf("从 %s:%d 收到消息: %s\n", client_ip, ntohs(client_addr.sin_port), buffer); } } } } return 0; }

本文作者:御坂19327号

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!