Linux 网络编程 您所在的位置:网站首页 recvfrom函数返回值为0 Linux 网络编程

Linux 网络编程

2023-03-12 14:05| 来源: 网络整理| 查看: 265

网络编程大体内容 IO进程 - 本地进程通信 两台不同主机如何不借助第三方软件通信 - 网络 linux下的网络编程 文件IO 标准IO - 文件(Linux下一切皆是文件,设备(字符设备、块设备)) 特殊设备

文章目录 一、网络发展史二、局域网和广域网2.1 光猫2.2 交换机和路由器2.3 网线 三、IP地址3.1 基本概念3.2 ip地址划分(IPv4)3.3 特殊地址3.4 子网掩码3.5 子网号概念 四、网络模型4.1 网络模型4.2 OSI模型4.3 TCP/IP模型4.4 常见网络协议 五、TCP与UDP六、socket简介6.1 socket类型6.2 socket的位置 七、端口号八、字节序8.1 主机字节序到网络字节序8.2 网络字节序到主机字节序8.3 IP地址转换 九、TCP编程9.1 三次握手流程图9.2 函数接口1、 socket创建套接字2、 bind绑定套接字3、 listen监听4、 accept等待连接5、 recv接收消息6、 send发送消息7、 connect发送连接8、 优化代码 十、实现:tcp实现ftp功能10.1 IO相关功能回顾10.2 代码实现 十一、UDP编程11.1 UDP流程图11.2 函数接口 十二、UDP网络聊天室12.1 项目要求12.2 问题思考12.3 程序流程图 十三、linux下I/O模型及特点13.1 阻塞式IO13.2 非阻塞式IO13.2.1 通过函数参数设置非阻塞:13.2.2 通过fcntl函数设置文件描述符属性设置非阻塞:13.3 信号驱动IO (异步IO模型 非重点)13.4 前三种使用场景假设总结13.5 IO多路复用13.5.1 IO多路复用场景假设13.5.2 IO多路复用机制13.5.3 实现IO多路复用的方式13.5.3.1 select 实现IO多路复用13.5.3.2 poll 实现IO多路复用13.5.3.3 epoll 实现IO多路复用(异步) 十四、服务器模型14.1 循环服务器模型14.2 并发服务器模型14.2.1 多进程模型14.2.2 多线程模型14.2.3 IO多路复用模型

一、网络发展史

【腾讯文档】internet历史

二、局域网和广域网

局域网(LAN) 局域网的缩写是LAN,local area network,顾名思义,是个本地的网络,只能实现小范围短距离的网络通信。我们的家庭网络是典型的局域网。电脑、手机、电视、智能音箱、智能插座都连在路由器上,可以互相通信。局域网,就像是小区里的道路,分支多,连接了很多栋楼。 广域网(Wan) 广域网(Wide Area Network)是相对局域网来讲的,局域网的传输距离比较近,只能是一个小范围的。如果需要长距离的传输,比如某大型企业,总部在北京,分公司在长沙,局域网是无法架设的。广域网,就像是大马路,分支可能少,但类型多,像国道、省道、高速、小道等,连接了很多大的局域网。

2.1 光猫

光猫是一种类似于基带modem(数字调制解调器)的设备,和基带modem不同的是接入的是光纤专线,是光信号。用于广域网中光电信号的转换和接口协议的转换,接入路由器,是广域网接入。 将光线插入左侧的灰色口,右侧网口接网线到路由器即可。

2.2 交换机和路由器

交换机(二层):用于局域网内网的数据转发路由器(三层):用于连接局域网和外网 路由器有交换机的功能,反之不成立,交换机没有IP分配和IP寻址的功能。 交换机各个口是平等的,所有接入的设备需要自己配置IP,然后组成局域网。 路由器需要区分WAN口和LAN口,WAN口是接外网的(从Modem出来的或者从上一级路由器出来的),LAN口是接内网的,现在路由器都带无线功能,本质上无线接入就是LAN。

2.3 网线

背过一种线序,了解网线的制作流程。 网线线序 网线制作方法

三、IP地址 3.1 基本概念

●IP地址是Internet中主机的标识 ● Internet中的主机要与别的机器通信必须具有一个IP地址 ● IP地址为32位(IPv4)或者128位(IPv6) ● 表示形式:常用点分形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。 ipv4 五类:A B C D E

3.2 ip地址划分(IPv4)

二级划分 ip=网络号+主机号 网络号:表示是否在一个网段内(局域网) 主机号:标识在本网段内的ID,同一局域网不能重复 ip地址取值范围:

A类:1.0.0.1~126.255.255.254 B类:128.0.0.1~~191.255.255.254 C类:192.0.0.1~~223.255.255.254 D类(组播地址):224.0.0.1~~239.255.255.254 E类:保留待用 11110

3.3 特殊地址

0.0.0.0:在服务器中,0.0.0.0指的是本机上的所有IPV4地址,如果一个主机有两个IP地址,192.168.1.1 和 10.1.2.1,并且该主机上的一个服务监听的地址是0.0.0.0,那么通过两个ip地址都能够访问该服务。 127.0.0.1:回环地址/环路地址,所有发往该类地址的数据包都应该被loop back。 每一个网段主机号为0的地址是网络地址,设置网关主机号为1的地址,主机号最大的地址是该网段的广播地址。全网广播地址255.255.255.255。

3.4 子网掩码

● 子网掩码:是一个32位的整数,作用是将某一个IP划分成网络地址和主机地址; ● 子网掩码长度是和IP地址长度完全一样; ● 网络号全为1,主机号全为0; 255.0.0.0 - A 255.255.0.0 - B 255.255.255.0 - C - 子网掩码 & ip地址 = 网络地址 (网段) ~子网掩码 & ip地址 = 主机地址

3.5 子网号概念

三级地址 ip=网络号+子网号+主机号 网络号+子网号 -网段(网络地址) 练习1: 某公司有四个部门:行政、研发1、研发2、营销,每个部门各30台计算机接入公司局域网交换机,如果要在192.168.1.0网段为每个部门划分子网,子网掩码应该怎么设置,每个子网的地址范围分别是什么?(4个部门之间不能通信) 256 - 4 = 64 => 2^6 192.168.1.0 - 254 子网掩码:255.255.255.1100 0000 - 主机号:0000 0000 - 1111 1111 00 0000 - 11 1111 》64 划分后: 子网掩码-》 255.255.255.192 将主机号划分两位作为网络号:取值-00 01 10 11 192.168.1. 00 网段 000000 - 111111 192.168.1.0 ~ 192.168.1.63 192.168.1.0网络地址 , 192.168.1.63广播地址 192.168.1. 01 网段 192.168.1. 64 ~ 192.168.1.127 192.168.1.64网络地址 , 192.168.1.127广播地址 192.168.1. 10 网段 192.168.1. 128 ~ 192.168.1. 191 192.168.1.128网络地址 , 192.168.1.191广播地址 192.168.1. 11 网段 192.168.1. 192~192.168.1. 255 192.168.1.192网络地址 , 192.168.1.255广播地址 划分为8个网段:每个网段链接30台 192.168.1.0 - 254 子网掩码:255.255.255.1110 0000 - 主机号:0000 0000 - 1111 1111 0 0000 - 1 1111 - >30 000 - 111 000 001 010 011 100 101 110 111

四、网络模型 4.1 网络模型

undefined 网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。 undefined 每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的。每层向上层提供服务,同时使用下层提供的服务 undefined 网络体系结构即指网络的层次结构和每层所使用协议的集合 undefined 两类非常重要的体系结构:OSI与TCP/IP

4.2 OSI模型

undefined OSI模型是一个理想化的模型,尚未有完整的实现 undefined OSI模型共有七层 undefined OSI现阶段只用作教学和理论研究

OSI模型 OSI模型是最理想的模型 物理层:传输的是bit流(0与1一样的数据),物理信号,没有格式 链路层:格式变为帧(把数据分成包,一帧一帧的数据进行发送) 网络层:路由器中是有算法的,ip,(主机到主机)(路由的转发) 传输层:端口号,数据传输到具体那个进程程序(端到端) 会话层:通信管理,负责建立或者断开通信连接 表示层:确保一个系统应用层发送的消息可以被另一个系统的应用层读取,编码转换,数据解析,管理数据加密,解密; 应用层:指定特定应用的协议,文件传输,文件管理,电子邮件等。 4.3 TCP/IP模型

TCP/IP参考模型 网络接口和物理层:屏蔽硬件差异(驱动),向上层提供统一的操作接口。 **网络层:**提供端对端的传输,可以理解为通过IP寻址机器。 **传输层:**决定数据交给机器的哪个任务(进程)去处理,通过端口寻址 应用层:应用协议和应用程序的集合 OSI和TCP/IP模型对应关系图 OSI参考模型

4.4 常见网络协议 网络接口和物理层: ppp:拨号协议(老式电话线上网方式) ARP:地址解析协议 IP-->MAC RARP:反向地址转换协议 MAC-->IP 网络层: IP(IPV4/IPV6):网间互连的协议 ICMP:网络控制管理协议,ping命令使用 IGMP:网络分组管理协议,广播和组播使用 传输层: TCP:传输控制协议 UDP:用户数据报协议 应用层: SSH:加密协议 telnet:远程登录协议 FTP:文件传输协议 HTTP:超文本传输协议 DNS:地址解析协议 SMTP/POP3:邮件传输协议

注意:TCP和IP是属于不同协议栈层的,只是这两个协议属于协议族里最重要的协议,所以协议栈或者模型以之命名了。

五、TCP与UDP

UDP TCP 协议相同点:都存在于传输层 TCP(即传输控制协议):

是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、 数据无失序、数据无重复到达的通信) 适用情况: 1、适合于对传输质量要求较高,以及传输大量数据的通信。 2、在需要可靠数据传输的场合,通常使用TCP协议 3、MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议

UDP (用户数据报协议):

UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。 适用情况: 1、发送小尺寸数据(如对DNS服务器进行IP地址查询时) 2、在接收到数据,给出应答较困难的网络中使用UDP。 3、适合于广播/组播式通信中。 4、MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议 5、流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输 六、socket简介 1》1982 - Berkeley Software Distributions 操作系统引入了socket作为本地进程之间通信的接口 2》1986 - Berkeley 扩展了socket 接口,使之支持UNIX 下的TCP/IP 通信 3》现在很多应用 (FTP, Telnet) 都依赖这一接口 Socket 1、是一个编程接口 2、是一种特殊的文件描述符 (everything in Unix is a file) 3、并不仅限于TCP/IP协议 4、面向连接 (Transmission Control Protocol - TCP/IP) 5、无连接 (User Datagram Protocol -UDP 和 Inter-network Packet Exchange - IPX) 为什么需要Socket? 普通的I/O操作过程 •打开文件->读/写操作->关闭文件 •TCP/IP协议被集成到操作系统的内核中,引入了新型的“I/O”操作 •进行网络通信的两个进程在不同的机器上,如何连接? •网络协议具有多样性,如何进行统一的操作 需要一种通用的网络编程接口:Socket 6.1 socket类型

流式套接字(SOCK_STREAM) TCP 提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。 数据报套接字(SOCK_DGRAM) UDP 提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。 原始套接字(SOCK_RAW) 可以对较低层次协议如IP、ICMP直接访问。

6.2 socket的位置

套接字就是网络进程的ID,可以认为套接字=网络地址ip+端口号。

七、端口号

● 为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区分 ● TCP端口号与UDP端口号独立 ● 端口号一般由IANA (Internet Assigned Numbers Authority) 管理 ● 端口用两个字节来表示 2byte

八、字节序

小端序(little-endian) - 低序字节存储在低地址 (主机字节序) 大端序(big-endian)- 高序字节存储在低地址 (网络字节序) 网络中传输的数据必须使用网络字节序,即大端字节序 笔试题:写一个函数,判断当前主机的字节序? 测试方式:共用体、指针强转、数据类型强转

#include union un { int a; short b; char c; }; int main(int argc, char const *argv[]) { union un st; st.a=0x12345678; printf("%#x %#x\n",st.b,st.c); #if 0 int a=0x12345678; //printf("%#x %#x\n",(char)a,(short)a); char *p=(char *)&a; printf("%#x %#x\n",*p,*(p+3)); #endif return 0; } 8.1 主机字节序到网络字节序 u_long htonl (u_long hostlong);//host to net u_short htons (u_short short); //掌握这个 8.2 网络字节序到主机字节序 u_long ntohl (u_long hostlong);//net to host u_short ntohs (u_short short); 8.3 IP地址转换 typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; }; in_addr_t inet_addr(const char *cp); //从人看的ip地址转为机器使用的32位无符号整数 char *inet_ntoa(struct in_addr in); //从机器到人,即net网络转addr地址 int inet_aton(const char *cp, struct int_addr *inp);//从人到机器 如:inet_aton(“192.168.1.0”,& mysock.sin_addr.s_addr); //设置地址

思考:Dos(拒绝式服务)攻击? (查阅) 拒绝服务攻击即是攻击者想办法让目标机器停止提供服务,是黑客常用的攻击手段之一。其实对网络带宽进行的消耗性攻击只是拒绝服务攻击的一小部分,只要能够对目标造成麻烦,使某些服务被暂停甚至主机死机,都属于拒绝服务攻击。拒绝服务攻击问题也一直得不到合理的解决,究其原因是因为网络协议本身的安全缺陷,从而拒绝服务攻击也成为了攻击者的终极手法。攻击者进行拒绝服务攻击,实际上让服务器实现两种效果:一是迫使服务器的缓冲区满,不接收新的请求;二是使用IP欺骗,迫使服务器把非法用户的连接复位,影响合法用户的连接。

九、TCP编程 9.1 三次握手流程图

服务器: socket:创建一个用与链接的套接字 bind:绑定自己的ip地址和端口 listen:监听,将主动套接字转为被动套接字 accept:阻塞等待客户端链接,链接成功返回一个用于通信套接字 recv:接收消息 send:发送消息 close:关闭文件描述符 客户端: socket:创建一个套接字 填充结构体:填充服务器的ip和端口 connect:阻塞等待链接服务器 recv/send:接收/发送消息 close:关闭 9.2 函数接口 1、 socket创建套接字 #include /* See NOTES */ #include int socket(int domain, int type, int protocol); 功能:创建套接字 参数: domain:协议族 AF_UNIX, AF_LOCAL 本地通信协议 unix(7) AF_INET IPv4 ip(7) AF_INET6 IPv6 ipv6(7) type:套接字类型 SOCK_STREAM 流式套接字 SOCK_DGRAM 数据报套接字 SOCK_RAW 原始套接字 protocol:协议 填0,自动匹配底层TCP或UDP等协议 系统默认自动帮助匹配对应协议 传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP 网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL) 返回值:成功文件描述符 失败-1 更新errno

2、 bind绑定套接字 #include /* See NOTES */ #include int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能:绑定套接字 - ip和端口 功能: sockfd:套接字文件描述符 addr:用于通信结构体 (提供的是通用结构体,需要根据选择通信方式, 填充对应结构体-通信结构体由socket第一个参数确定) addrlen:结构体大小 返回值: 成功:0 失败:-1 更新errno 通用结构体: struct sockaddr { sa_family_t sa_family; char sa_data[14]; } ipv4的通信结构体: struct sockaddr_in { sa_family_t sin_family; /*AF_INET */ in_port_t sin_port; /* 端口 */ struct in_addr sin_addr; /* ip地址 */ }; struct in_addr { uint32_t s_addr; }; 本地通信结构体: struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[108]; /* 套接字文件 */ };

IPV4结构体使用方式一: IPV4结构体使用方式二: IPV4结构体使用方式三:

#include #include #include #include int main(int argc,char **argv) { int sockfd; struct sockaddr_in mysock; sockfd = socket(AF_INET,SOCK_STREAM,0); //获得fd bzero(&mysock,sizeof(mysock)); //初始化结构体 mysock.sin_family = AF_INET; //设置地址家族 mysock.sin_port = htons(8080); //设置端口 inet_aton(“192.168.1.0”,& mysock.sin_addr.s_addr); //设置地址 // mysock.sin_addr.s_addr = inet_addr("192.168.1.0"); bind(sockfd,(struct sockaddr *)&mysock,sizeof(struct sockaddr); /* bind的时候进行转化 */ ... ... return 0; } 3、 listen监听 int listen(int sockfd, int backlog); 功能:监听,将主动套接字变为被动套接字,创建等待队列,用来存放未处理的客户连接请求。 参数: sockfd:套接字 backlog:同时响应客户端请求链接的最大个数,不能写0. 不同平台可同时链接的数不同,一般写6-8个 (队列1:保存正在连接) (队列2,连接上的客户端) 返回值:成功 0 失败-1,更新errno

4、 accept等待连接 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); accept(sockfd,NULL,NULL); 阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接, 则accept()函数返回,返回一个用于通信的套接字文件; 参数: Sockfd :套接字 addr: 链接客户端的ip和端口号 如果不需要关心具体是哪一个客户端,那么可以填NULL; addrlen:结构体的大小 如果不需要关心具体是哪一个客户端,那么可以填NULL; 返回值: 成功:文件描述符; //用于通信 失败:-1,更新errno

.会创建一个新的套接字文件描述符,后面的recv就是对新的套接字文件描述符操作。

5、 recv接收消息 ssize_t recv(int sockfd, void *buf, size_t len, int flags); 功能: 接收数据 参数: sockfd: acceptfd ; buf 存放位置 len 大小 flags 一般填0,相当于read()函数 MSG_DONTWAIT 非阻塞 返回值: 0 成功接收的字节个数 6、 send发送消息 ssize_t send(int sockfd, const void *buf, size_t len, int flags); 功能:发送数据 参数: sockfd:socket函数的返回值 buf:发送内容存放的地址 len:发送内存的长度 flags:如果填0,相当于write(); 返回值: 0 成功发送的字节个数 7、 connect发送连接 int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 功能:用于连接服务器; 参数: sockfd:socket函数的返回值 addr:填充的结构体是服务器端的; addrlen:结构体的大小 返回值 -1 失败,更新errno 正确 0

需要指明客户端是哪一个,accept如果不关心是哪个客户端,可以NULL忽略。 注:发送数据的大小最好与接收数据的大小一致。 server.c:

server.c:

8、 优化代码 1.去掉fget获取多余的‘\n’ if(buf[strlen(buf)-1]=='\n') buf[strlen(buf)-1]='\0'; 2.端口和ip地址通过命令行传参到代码中。 3.设置客户端退出,服务器结束循环接收。 通过recv返回值为0判断客户端是否退出 4.设置来电显示功能,获取到请求链接服务器的客户端的ip和端口。 5.设置服务器端自动获取自己的ip地址。 INADDR_ANY "0.0.0.0" 6.实现循环服务器,服务器不退出,当链接服务器的客户端退出,服务器等到下一个客户端链接。

十、实现:tcp实现ftp功能

模拟FTP核心原理:客户端连接服务器后,向服务器发送一个文件。文件名可以通过参数指定,服务器端接收客户端传来的文件(文件名随意),如果文件不存在自动创建文件,如果文件存在,那么清空文件然后写入。

项目功能介绍: 均有服务器和客户端代码,基于TCP写的。 在同一路径下,将客户端可执行代码复制到其他的路径下,接下来再不同的路径下运行服务器和客户端。 相当于另外一台电脑在访问服务器。 客户端和服务器链接成功后出现以下提示:四个功能 ***************list************** //列出服务器所在目录下的文件名(除目录不显示) ***********put filename********** //上传一个文件 ***********get filename********** //重服务器所在路径下载文件 **************quit*************** //退出(可只退出客户端,服务器等待下一个客户端链接) 10.1 IO相关功能回顾

笔者整理的语雀相关IO知识库回顾

10.2 代码实现

server.c

#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include int server_list(int acceptfd); int server_get(int acceptfd, char *file_p); int main(int argc, char const *argv[]) { if (argc != 2) { printf("please input %s \n", argv[0]); return -1; } //1.创建套接字 socket int sockfd, acceptfd; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd struct sockaddr_in * //2.绑定套接字 bind (绑定自己的ip和端口,便于别人找到自己) if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) perror("listen err."); return -1; } printf("listen ok.\n"); while (1) { //4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len); if (acceptfd //接收消息 recvbyte = recv(acceptfd, buf, sizeof(buf), 0); if (recvbyte printf("client exit.\n"); break; } else { printf("buf:%s\n", buf); switch (buf[0]) { case 'l': server_list(acceptfd); break; case 'g': server_get(acceptfd, buf + 4); break; } } } close(acceptfd); } close(sockfd); return 0; } //get filename //打开已存在的文件,读文件内容发送给客户端 int server_get(int acceptfd, char *file_p) { char buf[128]; int ret; int fd = open(file_p, O_RDONLY); if (fd ret=read(fd, buf, sizeof(buf)-1);//预留一个位置补\0 buf[ret]='\0'; if(ret == 0) break; send(acceptfd, buf, sizeof(buf), 0); } strcpy(buf, "send ok"); send(acceptfd, buf, sizeof(buf), 0); return 0; } int server_list(int acceptfd) { char buf[128]; struct stat st; struct dirent *file = NULL; DIR *dir = opendir("./"); if (dir == NULL) { perror("opendir err."); return -1; } while ((file = readdir(dir)) != NULL) { stat(file->d_name, &st); if ((st.st_mode & S_IFMT) == S_IFREG) { strcpy(buf, file->d_name); send(acceptfd, buf, sizeof(buf), 0); } } strcpy(buf, "send ok"); send(acceptfd, buf, sizeof(buf), 0); return 0; }

client.c

#include #include #include #include #include #include #include #include #include #include #include #include #include int client_list(int sockfd); void list(void); int client_get(int sockfd, char *file_p); int main(int argc, char const *argv[]) { if (argc != 3) { printf("please input %s \n", argv[0]); return -1; } //1.创建套接字 socket int sockfd; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd perror("connect err."); return -1; } printf("connect ok.\n"); //3.循环收发消息 char buf[128]; int recvbyte; while (1) { list(); fgets(buf, sizeof(buf), stdin); if (buf[strlen(buf) - 1] == '\n') buf[strlen(buf) - 1] = '\0'; if (strncmp(buf, "quit", 4) == 0) break; //发送请求 send(sockfd, buf, sizeof(buf), 0); switch (buf[0]) { case 'l': client_list(sockfd); break; case 'g': client_get(sockfd, buf + 4); break; } } close(sockfd); return 0; } //get filename //打开新建一个文件,接收写文件 int client_get(int sockfd, char *file_p) { char buf[128]; //1.打开新建文件 int fd = open(file_p, O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd recv(sockfd, buf, sizeof(buf), 0); if (strncmp(buf, "send ok", 7) == 0) break; //写文件 write(fd,buf,strlen(buf)); } return 0; } //list - 功能:显示服务器对应路径下的普通文件名 // 接收服务器发送过来的普通文件的名字 int client_list(int sockfd) { char buf[128] = ""; while (1) { recv(sockfd, buf, sizeof(buf), 0); if (strncmp(buf, "send ok", 7) == 0) break; printf("%s\n", buf); } return 0; } void list(void) { printf("+++++++++++++++++++++++++++++++++++++++\n"); printf("+++++++++++++ list +++++++++++++++++\n"); printf("+++++++++++++ get filename ++++++++++\n"); printf("+++++++++++++ put filename ++++++++++\n"); printf("+++++++++++++ quit ++++++++++++++++\n"); printf("+++++++++++++++++++++++++++++++++++++++\n"); } 十一、UDP编程 11.1 UDP流程图

udp流程:(类似发短信) server: 创建数据报套接字(socket(,SOCK_DGRAM,))----->有手机 绑定网络信息(bind())---------------------->绑定号码(发短信知道发给谁) 接收信息(recvfrom())--------------------->接收短信 关闭套接字(close())----------------------->接收完毕 client: 创建数据报套接字(socket())----------------------->有手机 指定服务器的网络信息------------------------------>有对方号码 发送信息(sendto())---------------------------->发送短信 关闭套接字(close())--------------------------->发送完 11.2 函数接口 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); 功能:接收数据 参数: sockfd:套接字描述符 buf:接收缓存区的首地址 len:接收缓存区的大小 flags:0 src_addr:发送端的网络信息结构体的指针 addrlen:发送端的网络信息结构体的大小的指针 返回值: 成功接收的字节个数 失败:-1 0:客户端退出 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 功能:发送数据 参数: sockfd:套接字描述符 buf:发送缓存区的首地址 len:发送缓存区的大小 flags:0 src_addr:接收端的网络信息结构体的指针 addrlen:接收端的网络信息结构体的大小 返回值: 成功发送的字节个数 失败:-1

server.c client.c 练习:实现如客户端发送"hello"给服务器端,服务器接着给客户端回,“recv:hello!!!”。

十二、UDP网络聊天室 注意: 1、对于TCP是先运行服务器,客户端才能运行。 2、对于UDP来说,服务器和客户端运行顺序没有先后,因为是无连接,所以服务器和客户端谁先开始,没有关系, 3、一个服务器可以同时连接多个客户端。想知道是哪个客户端登录,可以在服务器代码里面打印IP和端口号。 4、UDP,客户端当使用send的时候,上面需要加connect,这个connect不是代表连接的作用,而是指定客户端即将要发送给谁数据。这样就不需要使用sendto而用send就可以。 5、在TCP里面,也可以使用recvfrom和sendto,使用的时候将后面的两个参数都写为NULL就OK。 12.1 项目要求

利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。 12.2 问题思考

12.2 问题思考

● 客户端会不会知道其它客户端地址? UDP客户端不会直接互连,所以不会获知其它客户端地址,所有客户端地址存储在服务器端。 ● 有几种消息类型? **登录:**服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。 **聊天:**服务器只需要把某个客户端的聊天消息转发给所有其它客户端。 **退出:**服务器删除退出客户端的地址,并把退出消息发送给其它客户端。 ● 服务器如何存储客户端的地址? 数据结构可以选择线性数据结构

链表节点结构体: struct node{ struct sockaddr_in addr; struct node *next; }; 消息对应的结构体(同一个协议) typedef struct msg_t { int type;//L M Q char name[32];//用户名 char text[128];//消息正文 }MSG_t;

● 客户端如何同时处理发送和接收? 客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用gets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程或者多线程来处理。

12.3 程序流程图

服务器端 客户端

十三、linux下I/O模型及特点 13.1 阻塞式IO

特点:最简单、最常用;效率低

阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。 缺省情况下(及系统默认状态),套接字建立后所处于的模式就是阻塞I/O 模式。 学习的读写函数在调用过程中会发生阻塞相关函数如下: •读操作中的read、recv、recvfrom 读阻塞--》需要读缓冲区中有数据可读,读阻塞解除 •写操作中的write、send 写阻塞--》阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。 注意:sendto没有写阻塞 1)无sendto函数的原因: sendto不是阻塞函数,本身udp通信不是面向链接的,udp无发送缓冲区,即sendto没有发送缓冲区,send是有发送缓存区的,即sendto不是阻塞函数。 2)UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在缓冲区满的情况,在UDP套接字上进行写操作永远不会阻塞。 •其他操作:accept、connect 13.2 非阻塞式IO

特点:可以处理多路IO;需要轮询,浪费CPU资源

•当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。” •当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。 •应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。 •这种模式使用中不普遍。

13.2.1 通过函数参数设置非阻塞:

Recv函数最后一个参数写为0,为阻塞,写为MSG_DONTWAIT:表示非阻塞。

非阻塞,循环检测,是否有数据发过来,轮询消耗CPU资源。

13.2.2 通过fcntl函数设置文件描述符属性设置非阻塞: #include #include int fcntl(int fd, int cmd, ... /* arg */ ); 功能:设置文件描述符的属性 参数: fd:文件描述符 cmd:功能选择 - 更改状态属性 F_SETFLF 设置文件描述符属性 第三个参设设置 F_GETFL 获取文件描述符属性 第三个参数忽略 返回的是获取到的属性 arg:设置的属性值 返回值:失败 -1 更新errno 成功根据功能选择返回

fcntl.c

#include #include #include int main(int argc, char const *argv[]) { char buf[128]; //设置文件描述符0的非阻塞属性 int flags; flags = fcntl(0, F_GETFL); //获取原属性 flags |= O_NONBLOCK; //修改属性 fcntl(0, F_SETFL, flags); //修改的属性设置回去 while (1) { fgets(buf, sizeof(buf), stdin); printf("buf:%s\n", buf); } return 0; } 13.3 信号驱动IO (异步IO模型 非重点)

特点:异步通知模式,需要底层驱动的支持 ● 通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。 ● 应用程序收到信号后做异步处理即可。 ● 应用程序需要把自己的进程号告诉内核,并打开异步通知机制。 标准模板

//设置将APP进程号提交给内核驱动 fcntl(fd,F_SETOWN,getpid()); //设置异步通知 int flags; flags = fcntl(fd, F_GETFL); //获取原属性 flags |= O_ASYNC; //设置异步通知 fcntl(fd, F_SETFL, flags); //修改的属性设置回去 signal(SIGIO,handler);

举例:操作鼠标设备,当有输入的时候获取输入数据,没有输入时循环输出hello world。 鼠标设备路径 - /dev/input/mouse0 测试使用的是哪个鼠标设备:sudo cat /dev/input/mouse0 注意:执行代码时需要加sudo,普通用户没有操作设备的权限。

#include #include #include #include #include #include #include int fd; //信号处理函数 void handler(int sig) { char buf[128]; int ret = read(fd, buf, sizeof(buf) - 1); buf[ret] = '\0'; printf("mouse:%s\n", buf); } int main(int argc, char const *argv[]) { //1.打开鼠标设备 fd = open("/dev/input/mouse0", O_RDONLY); if (fd printf("hello world.\n"); sleep(1); } return 0; } 13.4 前三种使用场景假设总结

假设妈妈有一个孩子,孩子在房间里睡觉,妈妈需要及时获知孩子是否醒了,如何做?

进到房间陪着孩子一起睡觉,孩子醒了会吵醒妈妈:不累,但是不能干别的了时不时进房间看一下:简单,空闲时间还能干点别的,但是很累妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:互不耽误 13.5 IO多路复用 13.5.1 IO多路复用场景假设

假设妈妈有三个孩子,分别不同的房间里睡觉,需要及时获知每个孩子是否醒了,如何做?

不停进每个房间看一下:简单,空闲时间还能干点别的,但是很累把三个房间的门都打开,在客厅睡觉,同时监听所有房间的哭声,如果被哭声吵醒,那么能准确定位某个房间,及时处理即可:既能得到休息,也能及时获知每个孩子的状态。 13.5.2 IO多路复用机制

● 应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的; ● 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间; ● 若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂; ● 比较好的方法是使用I/O多路复用技术。其基本思想是: ○ 先构造一张有关描述符的表(最大1024),然后调用一个函数。 ○ 当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。 ○ 函数返回时告诉进程哪个描述符已就绪,可以进行I/O操作。

基本流程: 1. 先构造一张有关文件描述符的表(集合、数组); 2. 将你关心的文件描述符加入到这个表中; 3. 然后调用一个函数。 select / poll 4. 当这些文件描述符中的一个或多个已准备好进行I/O操作的时候 该函数才返回(阻塞)。 5. 判断是哪一个或哪些文件描述符产生了事件(IO操作); 6. 做对应的逻辑处理; 13.5.3 实现IO多路复用的方式 13.5.3.1 select 实现IO多路复用 int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 功能:select用于监测是哪个或哪些文件描述符产生事件; 有一个或多个同时产生时间返回值。 参数:nfds: 监测的最大文件描述个数 (这里是个数,使用的时候注意,与文件中最后一次打开的文件 描述符所对应的值的关系是什么?) readfds: 读事件集合; //读(用的多) writefds: 写事件集合; //NULL表示不关心 exceptfds:异常事件集合; timeout:超时检测 1 如果不做超时检测:传 NULL select返回值: 0 表示有事件产生; 如果设置了超时检测时间:&tv select返回值: 0 表示有事件产生; ==0 表示超时时间已到; struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; void FD_CLR(int fd, fd_set *set);//将fd从表中清除 int FD_ISSET(int fd, fd_set *set);//判断fd是否在表中 void FD_SET(int fd, fd_set *set);//将fd添加到表中 void FD_ZERO(fd_set *set);//清空表

select实现IO多路复用特点

1. 一个进程最多只能监听1024个文件描述符 (千级别) 2. select被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低(消耗CPU资源); 3. select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低(一个进程0~4G,0~3G是用户态,3G~4G是内核态,拷贝是非常耗时的);

练习:检测鼠标输入和键盘输入事件。 moues_key_select.c

#include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { //打开鼠标设备 int fd = open("/dev/input/mouse0", O_RDONLY); if (fd //3.调用select函数检测事件 tempfds=readfds; ret=select(maxfd+1,&tempfds,NULL,NULL,NULL); if(ret fgets(buf,sizeof(buf),stdin); printf("key:%s\n",buf); } //鼠标输入 if(FD_ISSET(fd,&tempfds)) { int ret=read(fd,buf,sizeof(buf)); printf("mouse:%s\n",buf); } } close(fd); return 0; }

练习2:检测键盘和sockfd (TCP实现同时链接多个客户端)

修改 serever.c代码: #include #include #include #include #include #include #include #include #include #include /* According to earlier standards */ #include #include #include int main(int argc, char const *argv[]) { if (argc != 2) { printf("please input %s \n", argv[0]); return -1; } //1.创建套接字 socket int sockfd, acceptfd; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd struct sockaddr_in * //2.绑定套接字 bind (绑定自己的ip和端口,便于别人找到自己) if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) perror("listen err."); return -1; } printf("listen ok.\n"); //select - 多个客户端链接一个服务器 用于链接的文件描述符是sockfd //请求,sockfd读到请求 检测sockfd的读事件 //1.创建表 fd_set readfds,tempfds; //清空表 FD_ZERO(&readfds); //2.将关心文件描述符添加到表 0 sockfd=3 FD_SET(0,&readfds); FD_SET(sockfd,&readfds); int maxfd=sockfd; //3.循环调用select检测 int ret; char buf[128]; while (1) { tempfds=readfds; ret=select(maxfd+1,&tempfds,NULL,NULL,NULL); if(ret fgets(buf,sizeof(buf),stdin); printf("key:%s\n",buf); } if(FD_ISSET(sockfd,&tempfds)) { //sockfd产生事件,有客户端链接,需要调用accept进行链接 //4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len); if (acceptfd if (argc != 2) { printf("please input %s \n", argv[0]); return -1; } //1.创建套接字 socket int sockfd, acceptfd; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd struct sockaddr_in * //2.绑定套接字 bind (绑定自己的ip和端口,便于别人找到自己) if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) perror("listen err."); return -1; } printf("listen ok.\n"); //功能:select 服务器响应多个客户端请求且进行通信 //1.创建表 fd_set readfds, tempfds; FD_ZERO(&readfds); //清空表 //2.添加关心的文件描述符 FD_SET(0, &readfds); FD_SET(sockfd, &readfds); int maxfd = sockfd, ret; char buf[128]; int recvbyte; //3.循环调用select检测 ,select阻塞等待有事件产生返回 while (1) { tempfds = readfds; ret = select(maxfd + 1, &tempfds, NULL, NULL, NULL); if (ret if (FD_ISSET(i, &tempfds))//循环遍历i即文件描述符的值,判断是否在监测表中 { if (i == 0)//监测标准输入 { fgets(buf, sizeof(buf), stdin); printf("key:%s\n", buf); //将服务器段输入的数据作为通知的消息发送给所有链接的客户端 for(int j=4;j send(j,buf,sizeof(buf),0); } } } else if (i == sockfd)//监测sockfd { //4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len); if (acceptfd //接收消息 recvbyte = recv(i, buf, sizeof(buf), 0); if (recvbyte printf("%d client exit.\n",i); FD_CLR(i, &readfds);//从文件描述符表中清除对应fd close(i); break; } else { printf("%d buf:%s\n",i, buf); } } } } } close(sockfd); return 0; }

client.c

#include #include #include #include #include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { if (argc != 3) { printf("please input %s \n", argv[0]); return -1; } //1.创建套接字 socket int sockfd; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd struct sockaddr_in * //2.请求链接 connect if (connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) perror("fork err."); return -1; } else if (pid == 0) { while (1) { fgets(buf, sizeof(buf), stdin); if (buf[strlen(buf) - 1] == '\n') buf[strlen(buf) - 1] = '\0'; //发送消息 send(sockfd, buf, sizeof(buf), 0); } } else { while (1) { //接收消息 recvbyte = recv(sockfd, buf, sizeof(buf), 0); if (recvbyte int fd; /* 检测的文件描述符 */ short events; /* 检测事件 */ short revents; /* 调用poll函数返回填充的事件,poll函数一旦返回,将对应事件自动填充结构体这个成员。只需要判断这个成员的值就可以确定是否产生事件 */ }; 使用如: struct pollfd fds[20]={};//poll()函数第一个参数传递的是fds的地址 事件: POLLIN :读事件 POLLOUT : 写事件 POLLERR:异常事件

poll实现IO多路复用的特点

1. 优化文件描述符个数的限制;(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组元素个数为1,如果想监听100个,那么这个结构体数组的元素个数就为100,由程序员自己来决定) 2. poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低 3. poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可

server.c

#include #include #include #include #include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { if (argc != 2) { printf("please input %s \n", argv[0]); return -1; } //1.创建套接字 socket int sockfd, acceptfd; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd struct sockaddr_in * //2.绑定套接字 bind (绑定自己的ip和端口,便于别人找到自己) if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) perror("listen err."); return -1; } printf("listen ok.\n"); //功能:poll 服务器响应多个客户端请求且进行通信 //1.创建表 -创建一个结构体数组 struct pollfd fds[20]={}; //大小自己确定,没有限定个数 //2.添加关心的文件描述符 fds[0].fd = 0; fds[0].events=POLLIN;//读事件 fds[1].fd=sockfd; fds[1].events=POLLIN; int n = 2, ret; char buf[128]; int recvbyte; //3.循环调用poll检测 ,poll阻塞等待有事件产生返回 -1 while (1) { ret = poll(fds, n, -1); if (ret if (fds[i].revents == POLLIN) { if (fds[i].fd == 0) { fgets(buf, sizeof(buf), stdin); printf("key:%s\n", buf); //将服务器段输入的数据作为通知的消息发送给所有链接的客户端 for(int j=2;j //4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len); if (acceptfd //接收消息 recvbyte = recv(fds[i].fd, buf, sizeof(buf), 0); if (recvbyte printf("%d client exit.\n",fds[i].fd); close(fds[i].fd); fds[i]=fds[n-1]; n--; i--; break; } else { printf("%d buf:%s\n",fds[i].fd, buf); } } } } } close(sockfd); return 0; } 13.5.3.3 epoll 实现IO多路复用(异步)

epoll实现机制:(了解)

epoll的提出--》它所支持的文件描述符上限是系统可以最大打开的文件的数目;eg:1GB机器上,这个上限10万个左右。 每个fd上面有callback(回调函数)函数,只有活跃的fd才有主动调用callback,不需要轮询。 注意: Epoll处理高并发,百万级,不关心底层怎样实现,只需要会调用就可以。

函数接口

#include int epoll_create(int size); 功能:创建红黑树根节点 参数:size:不作为实际意义值 >0 即可 返回值:成功时返回epoll文件描述符,失败时返回-1。 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 功能:控制epoll属性 epfd:epoll_create函数的返回句柄。 op:表示动作类型。有三个宏 来表示: EPOLL_CTL_ADD:注册新的fd到epfd中 EPOLL_CTL_MOD:修改已注册fd的监听事件 EPOLL_CTL_DEL:从epfd中删除一个fd Fd:需要监听的fd。 event:告诉内核需要监听什么事件 EPOLLIN:表示对应文件描述符可读 EPOLLOUT:可写 EPOLLPRI:有紧急数据可读; EPOLLERR:错误; EPOLLHUP:被挂断; EPOLLET:触发方式,边缘触发;(默认使用边缘触发) ET模式:表示状态的变化; 返回值:成功时返回0,失败时返回-1 typedef union epoll_data{ void* ptr;(无效) int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; / * Epoll事件* / epoll_data_t data; / *用户数据变量* / }; //等待事件到来 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 功能:等待事件的产生,类似于select的用法 epfd:句柄; events:用来保存从内核得到事件的集合; maxevents:表示每次能处理事件最大个数; timeout:超时时间,毫秒,0立即返回,-1阻塞 成功时返回发生事件的文件描述个数,失败时返回-1 帮助理解: 1.epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。 2.epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时, 返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中 依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。 3.另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl() 来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制, 迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

epoll实现IO多路复用的特点

•监听的最大的文件描述符没有个数限制(理论上,取决与你自己的系统) •异步I/O,Epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高 •epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.

server.c

#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { if (argc != 2) { printf("please input %s \n", argv[0]); return -1; } //1.创建套接字 socket int sockfd, acceptfd; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd struct sockaddr_in * //2.绑定套接字 bind (绑定自己的ip和端口,便于别人找到自己) if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) perror("listen err."); return -1; } printf("listen ok.\n"); //功能:epoll 服务器响应多个客户端请求且进行通信 struct epoll_event event; //暂时保存添加到树上的事件 struct epoll_event revents[20]; //保存从链表中获取的产生的事件 //1.创建表 -创建一颗树 int epfd = epoll_create(1); //2.添加关心的文件描述符到树上 epoll_ctl event.events = EPOLLIN | EPOLLET; event.data.fd = 0; epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event); event.events = EPOLLIN | EPOLLET; event.data.fd = sockfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event); int n = 2, ret; char buf[128]; int recvbyte; //3.循环调用epoll_wait检测链表中是否有事件 while (1) { //返回值是实际从链表中拿出来事件的个数 ret = epoll_wait(epfd, revents, 20, -1); if (ret if (revents[i].data.fd == 0) { fgets(buf, sizeof(buf), stdin); printf("key:%s\n", buf); } else if (revents[i].data.fd == sockfd) { //4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len); if (acceptfd //接收消息 recvbyte = recv(revents[i].data.fd, buf, sizeof(buf), 0); if (recvbyte printf("%d client exit.\n", revents[i].data.fd); close(revents[i].data.fd); epoll_ctl(epfd, EPOLL_CTL_DEL, revents[i].data.fd,NULL); break; } else { printf("%d buf:%s\n",revents[i].data.fd, buf); } } } } close(sockfd); return 0; } 十四、服务器模型

● 在网络程序里面,通常都是一个服务器处理多个客户机。 ● 为了处理多个客户机的请求, 服务器端的程序有不同的处理方式。

14.1 循环服务器模型

同一个时刻只能响应一个客户端的请求,伪代码如下:

socket() bind(); listen(); while(1) { accept(); while(1) { process(); //处理 } close(); } 14.2 并发服务器模型

同一个时刻可以响应多个客户端的请求,常用的模型有多进程模型/多线程模型/IO多路复用模型。

14.2.1 多进程模型

每来一个客户端连接,开一个子进程来专门处理客户端的数据,实现简单,但是系统开销相对较大,更推荐使用线程模型。伪代码如下:

socket() bind(); listen(); while(1) { accept(); if(fork() == 0) //子进程 { while(1) { process(); } close(client_fd); exit(); } else { } }

注意:收到客户端消息后,打印下是来自哪个客户端的数据(来电显示) 使用SIGCHLD来处理子进程结束的信号,信号函数中回收进程资源。

14.2.2 多线程模型

来一个客户端连接,开一个子线程来专门处理客户端的数据,实现简单,占用资源较少,属于使用比较广泛的模型:

socket() bind(); listen(); while(1) { accept(); pthread_create(); } 14.2.3 IO多路复用模型

借助select、poll、epoll机制,将新连接的客户端描述符增加到描述符表中,只需要一个线程即可处理所有的客户端连接,在嵌入式开发中应用广泛,不过代码写起了稍显繁琐。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有