协议的概念
什么是协议
从应用的角度出发,协议可理解为“规则”,是数据传输和数据的解释的规则。
假设,A、B双方欲传输文件。规定:
第一次,传输文件名,接收方接收到文件名,应答OK给传输方;
第二次,发送文件的尺寸,接收方接收到该数据再次应答一个OK;
第三次,传输文件内容。同样,接收方接收数据完成后应答OK表示文件内容接收成功。
由此,无论A、B之间传递何种文件,都是通过三次数据传输来完成。A、B之间形成了一个最简单的数据传输规则。双方都按此规则发送、接收数据。A、B之间达成的这个相互遵守的规则即为协议。
这种仅在A、B之间被遵守的协议称之为原始协议 。当此协议被更多的人采用,不断的增加、改进、维护、完善。最终形成一个稳定的、完整的文件传输协议,被广泛应用于各种文件传输过程中。该协议就成为一个标准协议 。最早的ftp协议就是由此衍生而来。
TCP协议注重数据的传输。http协议着重于数据的解释。
典型协议
传输层 常见协议有TCP/UDP协议。
应用层 常见的协议有HTTP协议,FTP协议。
网络层 常见协议有IP协议、ICMP协议、IGMP协议。
网络接口层 常见协议有ARP协议、RARP协议。
TCP传输控制协议 (Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层 通信协议。
UDP用户数据报协议(User Datagram Protocol)是OSI 参考模型中一种无连接的传输层 协议,提供面向事务的简单不可靠信息传送服务。
HTTP超文本传输协议 (Hyper Text Transfer Protocol)是互联网 上应用最为广泛的一种网络协议 。
FTP文件传输协议(File Transfer Protocol)
IP协议是因特网 互联协议(Internet Protocol)
ICMP协议是Internet控制报文 协议(Internet Control Message Protocol)它是TCP/IP协议族 的一个子协议,用于在IP主机 、路由 器之间传递控制消息。
IGMP协议是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
ARP 协议是正向地址解析协议 (Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址 。
RARP 是反向地址转换协议,通过MAC地址确定IP地址。
网络应用程序设计模式
C/S模式
传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。
优点:
协议选用灵活
可以提前缓存数据。
缺点:
1. 对用户的安全构成威胁
2. 开发工作量较大,调试困难
B/S模式
浏览器()/服务器(server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。
优点:
比较安全
开发工作简洁,只需要开发服务端
跨平台
缺点:
要求使用http协议
不可以提前缓存数据
分层模型
OSI七层模型
物理层 :主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特 。
数据链路层 :定义了如何让格式化数据以帧 为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、1
网络层 :在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。数据包
传输层 :定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段 。
会话层 :通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。
表示层 :可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换。
应用层 :是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。
TCP/IP四层模型
TCP/IP网络协议栈分为应用层(Application)、传输层(Transport)、网络层(Network)和链路层(Link)四层。如下图所示:
一般在应用开发过程中,讨论最多的是TCP/IP模型。
通信过程
两台计算机通过TCP/IP协议通讯的过程如下所示:
上图对应两台计算机在同一网段中的情况,如果两台计算机在不同的网段中,那么数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器,如下图所示:
链路层有以太网、令牌环网等标准,链路层负责网卡设备的驱动、帧同步(即从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验 等工作。交换机是工作在链路层的网络设备,可以在不同的链路层网络之间转发数据帧(比如十兆以太网和百兆以太网之间、以太网和令牌环网之间),由于不同链路层的帧格式不同,交换机要将进来的数据包拆掉链路层首部重新封装之后再转发。
网络层的IP协议是构成Internet的基础。Internet上的主机通过IP地址来标识,Inter-net上有大量路由器负责根据IP地址选择合适的路径转发数据包,数据包从Internet上的源主机到目的主机往往要经过十多个路由器。路由器是工作在第三层的网络设备,同时兼有交换机的功能,可以在不同的链路层接口之间转发数据包,因此路由器需要将进来的数据包拆掉网络层和链路层两层首部并重新封装。IP协议不保证传输的可靠性,数据包在传输过程中可能丢失,可靠性可以在上层协议或应用程序中提供支持。
网络层负责点到点(ptop,point-to-point)的传输(这里的“点”指主机或路由器),而传输层负责端到端(etoe,end-to-end)的传输(这里的“端”指源主机和目的主机)。传输层可选择TCP或UDP协议。
TCP是一种面向连接的、可靠的协议,有点像打电话,双方拿起电话互通身份之后就建立了连接,然后说话就行了,这边说的话那边保证听得到,并且是按说话的顺序听到的,说完话挂机断开连接。也就是说TCP传输的双方需要首先建立连接,之后由TCP协议保证数据收发的可靠性,丢失的数据包自动重发,上层应用程序收到的总是可靠的数据流,通讯之后关闭连接。
UDP是无连接的传输协议,不保证可靠性,有点像寄信,信写好放到邮筒里,既不能保证信件在邮递过程中不会丢失,也不能保证信件寄送顺序。使用UDP协议的应用程序需要自己完成丢包重发、消息排序等工作。
目的主机收到数据包后,如何经过各层协议栈最后到达应用程序呢?其过程如下图所示:
以太网驱动程序首先根据以太网首部中的“上层协议”字段确定该数据帧的有效载荷(payload,指除去协议首部之外实际传输的数据)是IP、ARP还是RARP协议的数据报,然后交给相应的协议处理。假如是IP数据报,IP协议再根据IP首部中的“上层协议”字段确定该数据报的有效载荷是TCP、UDP、ICMP还是IGMP,然后交给相应的协议处理。假如是TCP段或UDP段,TCP或UDP协议再根据TCP首部或UDP首部的“端口号”字段确定应该将应用层数据交给哪个用户进程。IP地址是标识网络中不同主机的地址,而端口号就是同一台主机上标识不同进程的地址,IP地址和端口号合起来标识网络中唯一的进程。
虽然IP、ARP和RARP数据报都需要以太网驱动程序来封装成帧,但是从功能上划分,ARP和RARP属于链路层,IP属于网络层。虽然ICMP、IGMP、TCP、UDP的数据都需要IP协议来封装成数据报,但是从功能上划分,ICMP、IGMP与IP同属于网络层,TCP和UDP属于传输层。
协议格式
数据包封装
传输层及其以下的机制由内核提供,应用层由用户进程提供(后面将介绍如何使用socket API编写应用程序),应用程序对通讯数据的含义进行解释,而传输层及其以下处理通讯的细节,将数据从一台计算机通过一定的路径发送到另一台计算机。应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation),如下图所示:
不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报(datagram),在链路层叫做帧(frame)。数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理。
以太网帧格式
以太网的帧格式如下所示:
其中的源地址和目的地址 是指网卡的硬件地址 (也叫MAC地址),长度是48位,是在网卡出厂时固化的。可在shell中使用ifconfig命令查看,“HWaddr 00:15:F2:14:9E:3F”部分就是硬件地址。协议字段有三种值,分别对应IP、ARP、RARP。帧尾是CRC校验码。
以太网帧中的数据长度规定最小46字节,最大1500字节,ARP和RARP数据包的长度不够46字节,要在后面补填充位。最大值1500称为以太网的最大传输单元(MTU) ,不同的网络类型有不同的MTU,如果一个数据包从以太网路由到拨号链路上,数据包长度大于拨号链路的MTU,则需要对数据包进行分片(fragmentation)。ifconfig命令输出中也有“MTU:1500”。注意,MTU这个概念指数据帧中有效载荷的最大长度,不包括帧头长度。
ARP数据报格式
在网络通讯时,源主机的应用程序知道目的主机的IP地址和端口号,却不知道目的主机的硬件地址,而数据包首先是被网卡接收到再去处理上层协议的,如果接收到的数据包的硬件地址与本机不符,则直接丢弃。因此在通讯前必须获得目的主机的硬件地址。ARP协议就起到这个作用。源主机发出ARP请求,询问“IP地址是192.168.0.1的主机的硬件地址是多少”,并将这个请求广播到本地网段(以太网帧首部的硬件地址填FF:FF:FF:FF:FF:FF表示广播),目的主机接收到广播的ARP请求,发现其中的IP地址与本机相符,则发送一个ARP应答数据包给源主机,将自己的硬件地址填写在应答包中。
每台主机都维护一个ARP缓存表,可以用arp -a命令查看。缓存表中的表项有过期时间(一般为20分钟),如果20分钟内没有再次使用某个表项,则该表项失效,下次还要发ARP请求来获得目的主机的硬件地址。想一想,为什么表项要有过期时间而不是一直有效?
ARP数据报的格式如下所示:
源MAC地址、目的MAC地址在以太网首部和ARP请求中各出现一次,对于链路层为以太网的情况是多余的,但如果链路层是其它类型的网络则有可能是必要的。硬件类型指链路层网络类型,1为以太网,协议类型指要转换的地址类型,0x0800为IP地址,后面两个地址长度对于以太网地址和IP地址分别为6和4(字节),op字段为1表示ARP请求,op字段为2表示ARP应答。
路由器会把arp数据报发给所有的机器,然后每个机器拿到后会检查接收端的ip是不是自己的ip,如果不是则扔掉
ARP数据报:获取下一条的mac地址
TTL:表示当前还可以进行多少跳,每次到达自己这里TTL都会减一
数据包到达路由器后会进行解封装,先解数据帧首尾,然后解网络层,然后再封装自己的ip和自己的mac地址,和目标mac地址,最后发出去,每次结果路由器都会重复上面步骤,直到到达目的pc
IP段格式
IP数据报的首部长度和数据长度都是可变长的,但总是4字节的整数倍 。对于IPv4,4位版本字段是4。4位首部长度的数值是以4字节为单位的,最小值为5 ,也就是说首部长度最小是4x5=20字节,也就是不带任何选项的IP首部,4位能表示的最大值是15,也就是说首部长度最大是60字节 。8位TOS字段有3个位用来指定IP数据报的优先级(目前已经废弃不用),还有4个位表示可选的服务类型(最小延迟、最大?吐量、最大可靠性、最小成本),还有一个位总是0。总长度是整个数据报(包括IP首部和IP层payload)的字节数。每传一个IP数据报,16位的标识加1,可用于分片和重新组装数据报。3位标志和13位片偏移用于分片。TTL(Time to live)是这样用的:源主机为数据包设定一个生存时间,比如64,每过一个路由器就把该值减1,如果减到0就表示路由已经太长了仍然找不到目的主机的网络,就丢弃该包,因此这个生存时间的单位不是秒,而是跳(hop)。协议字段指示上层协议是TCP、UDP、ICMP还是IGMP。然后是校验和,只校验IP首部,数据的校验由更高层协议负责。IPv4的IP地址长度为32位。
想一想,前面讲了以太网帧中的最小数据长度为46字节,不足46字节的要用填充字节补上,那么如何界定这46字节里前多少个字节是IP、ARP或RARP数据报而后面是填充字节?
UDP数据报格式
端口号表示指定的进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 下面分析一帧基于UDP的TFTP协议帧。 以太网首部 0000: 00 05 5d 67 d0 b1 00 05 5d 61 58 a8 08 00 IP首部 0000: 45 00 0010: 00 53 93 25 00 00 80 11 25 ec c0 a8 00 37 c0 a8 0020: 00 01 UDP首部 0020: 05 d4 00 45 00 3f ac 40 TFTP协议 0020: 00 01 'c'':''\''q' 0030: 'w''e''r''q''.''q''w''e'00 'n''e''t''a''s''c''i' 0040: 'i'00 'b''l''k''s''i''z''e'00 '5''1''2'00 't''i' 0050: 'm''e''o''u''t'00 '1''0'00 't''s''i''z''e'00 '0' 0060: 00以太网首部:源MAC地址是00:05:5d:61:58:a8,目的MAC地址是00:05:5d:67:d0:b1,上层协议类型0x0800表示IP。 IP首部:每一个字节0x45包含4位版本号和4位首部长度,版本号为4,即IPv4,首部长度为5,说明IP首部不带有选项字段。服务类型为0,没有使用服务。16位总长度字段(包括IP首部和IP层payload的长度)为0x0053,即83字节,加上以太网首部14字节可知整个帧长度是97字节。IP报标识是0x9325,标志字段和片偏移字段设置为0x0000,就是DF=0允许分片,MF=0此数据报没有更多分片,没有分片偏移。TTL是0x80,也就是128。上层协议0x11表示UDP协议。IP首部校验和为0x25ec,源主机IP是c0 a8 00 37(192.168.0.55),目的主机IP是c0 a8 00 01(192.168.0.1)。 UDP首部:源端口号0x05d4(1492)是客户端的端口号,目的端口号0x0045(69)是TFTP服务的well-known端口号。UDP报长度为0x003f,即63字节,包括UDP首部和UDP层pay-load的长度。UDP首部和UDP层payload的校验和为0xac40。 TFTP是基于文本的协议,各字段之间用字节0分隔,开头的00 01表示请求读取一个文件,接下来的各字段是: c:\qwerq.qwe netascii blksize 512 timeout 10 tsize 0
一般的网络通信都是像TFTP协议这样,通信的双方分别是客户端和服务器,客户端主动发起请求(上面的例子就是客户端发起的请求帧),而服务器被动地等待、接收和应答请求。客户端的IP地址和端口号唯一标识了该主机上的TFTP客户端进程,服务器的IP地址和端口号唯一标识了该主机上的TFTP服务进程,由于客户端是主动发起请求的一方,它必须知道服务器的IP地址和TFTP服务进程的端口号,所以,一些常见的网络协议有默认的服务器端口,例如HTTP服务默认TCP协议的80端口,FTP服务默认TCP协议的21端口,TFTP服务默认UDP协议的69端口(如上例所示)。在使用客户端程序时,必须指定服务器的主机名或IP地址,如果不明确指定端口号则采用默认端口,请读者查阅ftp、tftp等程序的man page了解如何指定端口号。/etc/services中列出了所有well-known的服务端口和对应的传输层协议,这是由IANA(Internet Assigned Numbers Authority)规定的,其中有些服务既可以用TCP也可以用UDP,为了清晰,IANA规定这样的服务采用相同的TCP或UDP默认端口号,而另外一些TCP和UDP的相同端口号却对应不同的服务。
很多服务有well-known的端口号,然而客户端程序的端口号却不必是well-known的,往往是每次运行客户端程序时由系统自动分配一个空闲的端口号,用完就释放掉,称为ephemeral的端口号,想想这是为什么?
前面提过,UDP协议不面向连接,也不保证传输的可靠性,例如:
发送端的UDP协议层只管把应用层传来的数据封装成段交给IP协议层就算完成任务了,如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息。
接收端的UDP协议层只管把收到的数据根据端口号交给相应的应用程序就算完成任务了,如果发送端发来多个数据包并且在网络上经过不同的路由,到达接收端时顺序已经错乱了,UDP协议层也不保证按发送时的顺序交给应用层。
通常接收端的UDP协议层将收到的数据放在一个固定大小的缓冲区中等待应用程序来提取和处理,如果应用程序提取和处理的速度很慢,而发送端发送的速度很快,就会丢失数据包,UDP协议层并不报告这种错误。
因此,使用UDP协议的应用程序必须考虑到这些可能的问题并实现适当的解决方案,例如等待应答、超时重发、为数据包编号、流量控制等。一般使用UDP协议的应用程序实现都比较简单,只是发送一些对可靠性要求不高的消息,而不发送大量的数据。例如,基于UDP的TFTP协议一般只用于传送小文件(所以才叫trivial的ftp),而基于TCP的FTP协议适用于 各种文件的传输。TCP协议又是如何用面向连接的服务来代替应用程序解决传输的可靠性问题呢。
TCP数据报格式
与UDP协议一样也有源端口号和目的端口号,通讯的双方由IP地址和端口号标识。32位序号、32位确认序号、窗口大小稍后详细解释。4位首部长度和IP协议头类似,表示TCP协议头的长度,以4字节为单位,因此TCP协议头最长可以是4x15=60字节,如果没有选项字段,TCP协议头最短20字节。URG、ACK、PSH、RST、SYN、FIN是六个控制位,本节稍后将解释SYN、ACK、FIN、RST四个位,其它位的解释从略。16位检验和将TCP协议头和数据都计算在内。紧急指针和各种选项的解释从略。
NAT映射
A机器发送数据包给服务器,首先数据包经过路由器,然后路由器会一个端口号和自己的ip再和A机器的ip成为映射关系,服务器收到后回A机器数据包,目的ip的地址是写路由器的ip,端口号写路由器创建出来的那个端口号,路由器收到数据包后会在NAT映射表中查找端口号对应的ip,找到后再发给A机器
打洞机制
两台机器在不同的局域网中进行直接通信,需要进行nat映射和打洞机制.
Socket编程
套接字概念
Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
套接字的内核实现较为复杂,不宜在学习初期深入学习。
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
套接字通信原理如下图所示:
在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。
TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。本章的主要内容是socket API,主要介绍TCP协议的函数接口,最后介绍UDP协议和UNIX Domain Socket的函数接口。
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发 出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如上一节的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换 。
1 2 3 4 5 6 7 #include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
IP地址转换函数
早期:
1 2 3 4 5 6 7 8 9 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp); in_addr_t inet_addr(const char *cp); char *inet_ntoa(struct in_addr in); 只能处理IPv4的ip地址 不可重入函数 注意参数是struct in_addr
现在:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <arpa/inet.h> int inet_pton (int af, const char *src, void *dst) ; 参数1 : AF_INET (ipv4) AF_INET6 (ipv6) const char *inet_ntop (int af, const void *src, char *dst, socklen_t size) ;参数1 : AF_INET (ipv4) AF_INET6 (ipv6) 两个函数错误返回-1 并设置errno
支持IPv4和IPv6
可重入函数
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr。
因此函数接口是void *addrptr。
sockaddr数据结构
strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
1 2 3 4 struct sockaddr { sa_family_t sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */ };
1 使用 sudo grep -r "struct sockaddr_in {" /usr 命令可查看到struct sockaddr_in结构体的定义。一般其默认的存储位置:/usr/include/linux/in.h 文件中。
一般使用这个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct sockaddr_in { __kernel_sa_family_t sin_family; /* Address family */ 地址结构类型 __be16 sin_port; /* Port number */ 端口号 struct in_addr sin_addr; /* Internet address */ IP地址 /* Pad to size of `struct sockaddr'. */ unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)]; }; struct in_addr { /* Internet address. */ __be32 s_addr; };
1 2 3 4 5 6 7 struct sockaddr_in6 { unsigned short int sin6_family; /* AF_INET6 */ __be16 sin6_port; /* Transport layer port # */ __be32 sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ __u32 sin6_scope_id; /* scope id (new in RFC2553) */ };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct in6_addr { union { __u8 u6_addr8[16]; __be16 u6_addr16[8]; __be32 u6_addr32[4]; } in6_u; #define s6_addr in6_u.u6_addr8 #define s6_addr16 in6_u.u6_addr16 #define s6_addr32 in6_u.u6_addr32 }; #define UNIX_PATH_MAX 108 struct sockaddr_un { __kernel_sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
Pv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h中,用sock-addr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和Unix Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:
1 2 struct sockaddr_in servaddr; bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)); /* initialize servaddr */
网络套接字函数
socket模型创建流程图
socket函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); domain: AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址 AF_INET6 与上面类似,不过是来用IPv6的地址 AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用 type: SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。 SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。 SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。 SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议) SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序 protocol: 传0 表示使用默认协议。 返回值: 成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
bind函数
1 2 3 4 5 6 7 8 9 10 11 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); sockfd: socket文件描述符 addr: 构造出IP地址加端口号 addrlen: sizeof(addr)长度 返回值: 成功返回0,失败返回-1, 设置errno
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:
1 2 3 4 5 struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(6666);
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址 ,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。
listen函数
表示最多同时能有多少客户端连接
1 2 3 4 5 6 7 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); sockfd: socket文件描述符 backlog: 排队建立3次握手队列和刚刚建立3次握手队列的链接数和
查看系统默认backlog
1 cat /proc/sys/net/ipv4/tcp_max_syn_backlog
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
accept函数
1 2 3 4 5 6 7 8 9 10 11 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); sockdf: socket文件描述符 addr: 传出参数,返回链接客户端地址信息,含IP地址和端口号 addrlen: 传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小 返回值: 成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。
我们的服务器程序结构是这样的:
1 2 3 4 5 6 7 while (1) { cliaddr_len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); n = read(connfd, buf, MAXLINE); ...... close(connfd); }
整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1。
connect函数
1 2 3 4 5 6 7 8 9 10 11 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); sockdf: socket文件描述符 addr: 传入参数,指定服务器端地址信息,含IP地址和端口号 addrlen: 传入参数,传入sizeof(addr)大小 返回值: 成功返回0,失败返回-1,设置errno
C/S模型-TCP
下图是基于TCP协议的客户端/服务器程序的一般流程:
服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
在学习socket API时要注意应用程序和TCP协议层是如何交互的: 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
nc命令
简单的客户端程序
1 2 3 nc ip地址 端口号 nc 127.0.0.1 6666 nc 127.1 6666
bzero函数
1 2 3 4 5 6 7 //要包含的头文件 #include <string.h> // 作用: 把缓冲区逐字节清零 void bzero(void *s, size_t n); void *s //指定缓冲区 size_t n //指定缓冲取大小
server实现
作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <stdlib.h> #include <arpa/inet.h> #include <ctype.h> #define SERV_IP "127.0.0.1" #define SERV_PORT 6666 int main (void ) { int lfd, cfd; struct sockaddr_in serv_addr , clie_addr ; socklen_t clie_addr_len; char buf[BUFSIZ],CLIE_IP[BUFZIZ]; int n, i; lfd = socket(AF_INET, SOCK_STREAM, 0 ); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); bind(lfd, (struct sockaddr *)&serv_addr, sizeof (serv_addr)); listen(lfd, 128 ); clie_addr_len = sizeof (clie_addr); cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len); printf ("client IP: %s port:%d\n" , inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof (clie_IP)), ntohs(clie_addr.sin_port)); while (1 ) { n = read(cfd, buf, sizeof (buf)); for (i = 0 ; i < n; i++) buf[i] = toupper (buf[i]); write(cfd, buf, n); } close(lfd); close(cfd); return 0 ; }
client客户端实现
作用:客户端输入英文小写字符串,并发送给服务端,然后服务端回一串对应的大写英文字符串,最后输出到终端
如果客户端不绑定端口和ip,操作系统会隐式的帮客户端绑定地址和端口
1 2 3 man 7 ip //查看struct sockaddr_in 结构体 gets函数最好不要使用,因为已经过时了,使用fgets来代替gets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #define SERV_IP "127.0.0.1" #define SERV_PORT 9527 int main (void ) { int sfd, len; struct sockaddr_in serv_addr ; char buf[BUFSIZ]; sfd = socket(AF_INET, SOCK_STREAM, 0 ); bzero(&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); serv_addr.sin_port = htons(SERV_PORT); connect(sfd, (struct sockaddr *)&serv_addr, sizeof (serv_addr)); while (1 ) { fgets(buf, sizeof (buf), stdin ); write(sfd, buf, strlen (buf)); len = read(sfd, buf, sizeof (buf)); write(STDOUT_FILENO, buf, len); } close(sfd); return 0 ; }
客户端服务器程序分析
客户端和服务器端的cfd文件描述符各自指向两个缓冲区一个是读缓冲区另一个是写缓冲区
他们是通过IP地址+端口号进行建立连接的
一个文件描述符读的同时也可以写,所以是双向全双工的
客户端输入hello然后通过cfd写入发送缓冲区里,然后发送给服务器端的cfd接收缓冲区,服务器端收到hello后将其转换为HELLO后,将其写入发送缓冲区,然后发送给客户端的接收缓冲区,客户端收到后显示到终端
如果cfd只有一个缓冲区,就只能接收或发送,如果缓冲区是负责接收的,那么就不能发送,相反同上。所以cfd一定是两个缓冲区进行工作的。
服务器端或客户端的read如果没有接收到消息,一定是阻塞的,直到缓冲区收到内容才进行工作。
查看网络程序端口
错误处理函数
帮助文档查看函数名的时候是不区分大小写的
wrap.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #ifndef __WRAP_H_ #define __WRAP_H_ void perr_exit (const char *s) ;int Accept (int fd, struct sockaddr *sa, socklen_t *salenptr) ;int Bind (int fd, const struct sockaddr *sa, socklen_t salen) ;int Connect (int fd, const struct sockaddr *sa, socklen_t salen) ;int Listen (int fd, int backlog) ;int Socket (int family, int type, int protocol) ;ssize_t Read(int fd, void *ptr, size_t nbytes);ssize_t Write(int fd, const void *ptr, size_t nbytes);int Close (int fd) ;ssize_t Readn(int fd, void *vptr, size_t n);ssize_t Writen(int fd, const void *vptr, size_t n);ssize_t my_read(int fd, char *ptr);ssize_t Readline(int fd, void *vptr, size_t maxlen);#endif
wrap.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> void perr_exit (const char *s) { perror(s); exit (-1 ); } int Accept (int fd, struct sockaddr *sa, socklen_t *salenptr) { int n; again: if ((n = accept(fd, sa, salenptr)) < 0 ) { if ((errno == ECONNABORTED) || (errno == EINTR)) goto again; else perr_exit("accept error" ); } return n; } int Bind (int fd, const struct sockaddr *sa, socklen_t salen) { int n; if ((n = bind(fd, sa, salen)) < 0 ) perr_exit("bind error" ); return n; } int Connect (int fd, const struct sockaddr *sa, socklen_t salen) { int n; n = connect(fd, sa, salen); if (n < 0 ) { perr_exit("connect error" ); } return n; } int Listen (int fd, int backlog) { int n; if ((n = listen(fd, backlog)) < 0 ) perr_exit("listen error" ); return n; } int Socket (int family, int type, int protocol) { int n; if ((n = socket(family, type, protocol)) < 0 ) perr_exit("socket error" ); return n; } ssize_t Read(int fd, void *ptr, size_t nbytes){ ssize_t n; again: if ( (n = read(fd, ptr, nbytes)) == -1 ) { if (errno == EINTR) goto again; else return -1 ; } return n; } ssize_t Write(int fd, const void *ptr, size_t nbytes){ ssize_t n; again: if ((n = write(fd, ptr, nbytes)) == -1 ) { if (errno == EINTR) goto again; else return -1 ; } return n; } int Close (int fd) { int n; if ((n = close(fd)) == -1 ) perr_exit("close error" ); return n; } ssize_t Readn(int fd, void *vptr, size_t n){ size_t nleft; ssize_t nread; char *ptr; ptr = vptr; nleft = n; while (nleft > 0 ) { if ((nread = read(fd, ptr, nleft)) < 0 ) { if (errno == EINTR) nread = 0 ; else return -1 ; } else if (nread == 0 ) break ; nleft -= nread; ptr += nread; } return n - nleft; } ssize_t Writen(int fd, const void *vptr, size_t n){ size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; nleft = n; while (nleft > 0 ) { if ( (nwritten = write(fd, ptr, nleft)) <= 0 ) { if (nwritten < 0 && errno == EINTR) nwritten = 0 ; else return -1 ; } nleft -= nwritten; ptr += nwritten; } return n; } static ssize_t my_read (int fd, char *ptr) { static int read_cnt; static char *read_ptr; static char read_buf[100 ]; if (read_cnt <= 0 ) { again: if ( (read_cnt = read(fd, read_buf, sizeof (read_buf))) < 0 ) { if (errno == EINTR) goto again; return -1 ; } else if (read_cnt == 0 ) return 0 ; read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; return 1 ; } ssize_t Readline(int fd, void *vptr, size_t maxlen){ ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1 ; n < maxlen; n++) { if ((rc = my_read(fd, &c)) == 1 ) { *ptr++ = c; if (c == '\n' ) break ; } else if (rc == 0 ) { *ptr = 0 ; return n-1 ; } else return -1 ; } *ptr = 0 ; return n; }
read函数返回值
1 2 3 4 5 6 7 8 char buf[1024]; >0 实际读到的字节数,可以等于buf或小于buf =0 对端关闭 =-1 异常 1. errno == EINTR 被信号中断 可以进行重启或退出处理 2. errno == EAGAIN (EWOULDBLOCK) 非阻塞方式读,并且没有数据 3. 其他值 出现错误。--perror 打印错误信息 ,exit 退出程序。
TCP三次握手和四次挥手
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 TCP三次握手 客户端: 发送SYN包 1(0) 括号0代表这个数据包带0字节数据 服务端: 收到后做应答 2000(0) ACK 2 ACK是应答客户端发送的SYN包号+1 客户端: 收到服务端的包后做出应答 ACK 2001 TCP四次挥手 客户端: 发送FIN包 2(0) ACK 2001 服务端; 收到后回一个ACK包 ACK 3 这时候TCP属于半关闭状态 服务端: 发送FIN包 FIN 2001(0) ACK 3 客户端: 发送ACK包 ACK 2002 MTU、mss、半关闭 MTU: 最大传输单元 受协议限制 以太网1500 IP 65535 mss: 受MTU 标示一个数据包携带数据的上限数。 win: 滑动窗口——当前本端 能接收的数据上限值。(单位:字节)
使用TCP建立连接的机器,如果机器A发送数据包给机器B,机器B没有收到的话,机器A会重新发送一次
协议上限分析
多进程并发服务器程序实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <ctype.h> #include <unistd.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 void do_sigchild (int num) { while (waitpid(0 , NULL , WNOHANG) > 0 ) ; } int main (void ) { struct sockaddr_in servaddr , cliaddr ; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; pid_t pid; struct sigaction newact ; newact.sa_handler = do_sigchild; sigemptyset(&newact.sa_mask); newact.sa_flags = 0 ; sigaction(SIGCHLD, &newact, NULL ); listenfd = Socket(AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof (servaddr)); Listen(listenfd, 20 ); printf ("Accepting connections ...\n" ); while (1 ) { cliaddr_len = sizeof (cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); pid = fork(); if (pid == 0 ) { Close(listenfd); while (1 ) { n = Read(connfd, buf, MAXLINE); if (n == 0 ) { printf ("the other side has been closed.\n" ); break ; } printf ("received from %s at PORT %d\n" , inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof (str)), ntohs(cliaddr.sin_port)); for (i = 0 ; i < n; i++) buf[i] = toupper (buf[i]); Write(STDOUT_FILENO, buf, n); Write(connfd, buf, n); } Close(connfd); return 0 ; } else if (pid > 0 ) { Close(connfd); } else perr_exit("fork" ); } return 0 ; }
多线程并发服务器程序实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 #include <stdio.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #include <ctype.h> #include <unistd.h> #include <fcntl.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 struct s_info { struct sockaddr_in cliaddr ; int connfd; }; void *do_work (void *arg) { int n,i; struct s_info *ts = (struct s_info *)arg ; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; while (1 ) { n = Read(ts->connfd, buf, MAXLINE); if (n == 0 ) { printf ("the client %d closed...\n" , ts->connfd); break ; } printf ("received from %s at PORT %d\n" , inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof (str)), ntohs((*ts).cliaddr.sin_port)); for (i = 0 ; i < n; i++) buf[i] = toupper (buf[i]); Write(STDOUT_FILENO, buf, n); Write(ts->connfd, buf, n); } Close(ts->connfd); return (void *)0 ; } int main (void ) { struct sockaddr_in servaddr , cliaddr ; socklen_t cliaddr_len; int listenfd, connfd; pthread_t tid; struct s_info ts [256]; int i = 0 ; listenfd = Socket(AF_INET, SOCK_STREAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof (servaddr)); Listen(listenfd, 128 ); printf ("Accepting client connect ...\n" ); while (1 ) { cliaddr_len = sizeof (cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); ts[i].cliaddr = cliaddr; ts[i].connfd = connfd; pthread_create(&tid, NULL , do_work, (void *)&ts[i]); pthread_detach(tid); i++; } return 0 ; }
TCP状态转换
RST标志位表示客户端异常断开,服务端无法接收ACK,所以客户端操作系统会发送一个RST标志,然后让服务端回到LISTEN状态,重新接收连接
2MSL在linux中是
TIME_WAIT状态是确保主动关闭端发送的最后一个ACK能顺利到达
CLOSED:表示初始状态。
LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。
SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。
ESTABLISHED:表示连接已经建立。
FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是:
FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。
FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。
FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。该状态下的socket只能接收数据,不能发。
TIME_WAIT:表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。
LAST_ACK:该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态。
2MSL
2MSL (Maximum Segment Lifetime)
RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。
程序设计中的问题
做一个测试,首先启动server,然后启动client,用Ctrl-C终止server,马上再运行server,运行结果:
1 2 itcast$ ./server bind error: Address already in use
这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:
1 2 3 itcast$ netstat -apn |grep 6666 tcp 1 0 192.168.1.11:38103 192.168.1.11:6666 CLOSE_WAIT 3525/client tcp 0 0 192.168.1.11:6666 192.168.1.11:38103 FIN_WAIT2 -
server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。
现在用Ctrl-C把client也终止掉,再观察现象:
1 2 3 4 itcast$ netstat -apn |grep 6666 tcp 0 0 192.168.1.11:6666 192.168.1.11:38104 TIME_WAIT - itcast$ ./server bind error: Address already in use
client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态 ,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。
MSL在RFC 1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。至于为什么要规定TIME_WAIT的时间,可参考UNP 2.7节。
半关闭
当TCP链接中A发送FIN请求关闭,B端回应ACK后(A端进入FIN_WAIT_2状态),B没有立即发送FIN给A时,A方处在半链接状态,此时A可以接收B发送的数据,但是A已不能再向B发送数据。
从程序的角度,可以使用API来控制实现半连接状态。
1 2 3 4 5 6 7 8 #include <sys/socket.h> int shutdown(int sockfd, int how); sockfd: 需要关闭的socket的描述符 how: 允许为shutdown操作选择以下几种方式: SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。 该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。 SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。 SHUT_RDWR(2): 关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。
使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。
shutdown不考虑描述符的引用计数,直接关闭描述符 。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。
在多进程中如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但,如果一个进程close(sfd)将不会影响到其它进程。
端口复用
在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为,TCP连接没有完全断开指的是connfd(127.0.0.1:6666)没有完全断开,而我们重新监听的是lis-tenfd(0.0.0.0:6666),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
在server代码的socket()和bind()调用之间插入如下代码:
1 2 int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
有关setsockopt可以设置的其它选项请参考UNP第7章。
多路I/O转接服务器
多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
主要使用的方法有三种
select
select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力
如果要修改文件描述符上限需要重新编译内核
如果有两个文件描述符,一个是1号文件描述符另一个是1023号文件描述符触发了读事件,则需要for循环遍历1023次才能找到对应的两个事件,解决办法是创建一个数组来保存文件描述符,防止多余的遍历
满足监听条件的集合和原有集合都是同一个集合,所有要提前保存好原有集合,防止数据被覆盖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <sys/select.h> /* According to earlier standards */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态 readfds: 监控有读数据到达文件描述符集合,传入传出参数 writefds: 监控写数据到达文件描述符集合,传入传出参数 exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数 timeout: 定时阻塞监控时间,3种情况 1.NULL,永远等下去 2.设置timeval,等待固定时间 3.设置timeval里时间均为0,检查描述字后立即返回,轮询 struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0 int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1 void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1 void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
select实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <ctype.h> #include "wrap.h" #define SERV_PORT 6666 int main (int argc, char *argv[]) { int i, j, n, maxi; int nready, client[FD_SETSIZE]; int maxfd, listenfd, connfd, sockfd; char buf[BUFSIZ], str[INET_ADDRSTRLEN]; struct sockaddr_in clie_addr , serv_addr ; socklen_t clie_addr_len; fd_set rset, allset; listenfd = Socket(AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); bzero(&serv_addr, sizeof (serv_addr)); serv_addr.sin_family= AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port= htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof (serv_addr)); Listen(listenfd, 128 ); maxfd = listenfd; maxi = -1 ; for (i = 0 ; i < FD_SETSIZE; i++) client[i] = -1 ; FD_ZERO(&allset); FD_SET(listenfd, &allset); while (1 ) { rset = allset; nready = select(maxfd+1 , &rset, NULL , NULL , NULL ); if (nready < 0 ) perr_exit("select error" ); if (FD_ISSET(listenfd, &rset)) { clie_addr_len = sizeof (clie_addr); connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); printf ("received from %s at PORT %d\n" , inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, str, sizeof (str)), ntohs(clie_addr.sin_port)); for (i = 0 ; i < FD_SETSIZE; i++) if (client[i] < 0 ) { client[i] = connfd; break ; } if (i == FD_SETSIZE) { fputs ("too many clients\n" , stderr ); exit (1 ); } FD_SET(connfd, &allset); if (connfd > maxfd) maxfd = connfd; if (i > maxi) maxi = i; if (--nready == 0 ) continue ; } for (i = 0 ; i <= maxi; i++) { if ((sockfd = client[i]) < 0 ) continue ; if (FD_ISSET(sockfd, &rset)) { if ((n = Read(sockfd, buf, sizeof (buf))) == 0 ) { Close(sockfd); FD_CLR(sockfd, &allset); client[i] = -1 ; } else if (n > 0 ) { for (j = 0 ; j < n; j++) buf[j] = toupper (buf[j]); Write(sockfd, buf, n); Write(STDOUT_FILENO, buf, n); } if (--nready == 0 ) break ; } } } Close(listenfd); return 0 ; }
poll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); struct pollfd { int fd; /* 文件描述符 */ short events; /* 监控的事件 */ short revents; /* 监控事件中满足条件返回的事件 */ }; POLLIN 普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND POLLRDNORM 数据可读 POLLRDBAND 优先级带数据可读 POLLPRI 高优先级可读数据 POLLOUT 普通或带外数据可写 POLLWRNORM 数据可写 POLLWRBAND 优先级带数据可写 POLLERR 发生错误 POLLHUP 发生挂起 POLLNVAL 描述字不是一个打开的文件 nfds 监控数组中有多少文件描述符需要被监控 timeout 毫秒级等待 -1:阻塞等,#define INFTIM -1 Linux中没有定义此宏 0:立即返回,不阻塞进程 >0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值
如果不再监控某个文件描述符时,可以把pollfd中,fd设置为-1,poll不再监控此pollfd,下次返回时,把revents设置为0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <poll.h> #include <errno.h> #include <ctype.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 #define OPEN_MAX 1024 int main (int argc, char *argv[]) { int i, j, maxi, listenfd, connfd, sockfd; int nready; ssize_t n; char buf[MAXLINE], str[INET_ADDRSTRLEN]; socklen_t clilen; struct pollfd client [OPEN_MAX ]; struct sockaddr_in cliaddr , servaddr ; listenfd = Socket(AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof (servaddr)); Listen(listenfd, 128 ); client[0 ].fd = listenfd; client[0 ].events = POLLIN; for (i = 1 ; i < OPEN_MAX; i++) client[i].fd = -1 ; maxi = 0 ; for ( ; ; ) { nready = poll(client, maxi+1 , -1 ); if (client[0 ].revents & POLLIN) { clilen = sizeof (cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); printf ("received from %s at PORT %d\n" , inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof (str)), ntohs(cliaddr.sin_port)); for (i = 1 ; i < OPEN_MAX; i++) if (client[i].fd < 0 ) { client[i].fd = connfd; break ; } if (i == OPEN_MAX) perr_exit("too many clients" ); client[i].events = POLLIN; if (i > maxi) maxi = i; if (--nready <= 0 ) continue ; } for (i = 1 ; i <= maxi; i++) { if ((sockfd = client[i].fd) < 0 ) continue ; if (client[i].revents & POLLIN) { if ((n = Read(sockfd, buf, MAXLINE)) < 0 ) { if (errno == ECONNRESET) { printf ("client[%d] aborted connection\n" , i); Close(sockfd); client[i].fd = -1 ; } else perr_exit("read error" ); } else if (n == 0 ) { printf ("client[%d] closed connection\n" , i); Close(sockfd); client[i].fd = -1 ; } else { for (j = 0 ; j < n; j++) buf[j] = toupper (buf[j]); Writen(sockfd, buf, n); } if (--nready <= 0 ) break ; } } } return 0 ; }
epoll
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
目前epell是linux大规模并发网络程序中的热门首选模型。
epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
可以使用cat命令查看一个进程可以打开的socket描述符上限。
1 cat /proc/sys/fs/file-max
如有需要,可以通过修改配置文件的方式修改该上限值。
1 2 3 4 sudo vi /etc/security/limits.conf 在文件尾部写入以下配置,soft软限制,hard硬限制。如下图所示。 * soft nofile 65536 * hard nofile 100000
基础API
创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。
1 2 3 4 5 6 7 #include <sys/epoll.h> int epoll_create(int size) size:监听数目 参数说明: size: 最大节点数, 此参数在linux 2.6.8已被忽略, 但必须传递一个大于0的数. 返回值: 成功: 返回一个大于0的文件描述符, 代表整个树的树根. 失败: 返回-1, 并设置errno值.
控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) epfd: 为epoll_creat的句柄 op: 表示动作,用3个宏来表示: EPOLL_CTL_ADD (注册新的fd到epfd), EPOLL_CTL_MOD (修改已经注册的fd的监听事件), EPOLL_CTL_DEL (从epfd删除一个fd); event: 告诉内核需要监听的事件 struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭) EPOLLOUT: 表示对应的文件描述符可以写 EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来) EPOLLERR: 表示对应的文件描述符发生错误 EPOLLHUP: 表示对应的文件描述符被挂断; EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
等待所监控文件描述符上有事件的产生,类似于select()调用。
1 2 3 4 5 6 7 8 9 #include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) events: 用来存内核得到事件的集合, maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size, timeout: 是超时时间 -1: 阻塞 0: 立即返回,非阻塞 >0: 指定毫秒 返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <sys/epoll.h> #include <errno.h> #include <ctype.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 #define OPEN_MAX 5000 int main (int argc, char *argv[]) { int i, listenfd, connfd, sockfd; int n, num = 0 ; ssize_t nready, efd, res; char buf[MAXLINE], str[INET_ADDRSTRLEN]; socklen_t clilen; struct sockaddr_in cliaddr , servaddr ; struct epoll_event tep , ep [OPEN_MAX ]; listenfd = Socket(AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *) &servaddr, sizeof (servaddr)); Listen(listenfd, 20 ); efd = epoll_create(OPEN_MAX); if (efd == -1 ) perr_exit("epoll_create error" ); tep.events = EPOLLIN; tep.data.fd = listenfd; res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); if (res == -1 ) perr_exit("epoll_ctl error" ); for ( ; ; ) { nready = epoll_wait(efd, ep, OPEN_MAX, -1 ); if (nready == -1 ) perr_exit("epoll_wait error" ); for (i = 0 ; i < nready; i++) { if (!(ep[i].events & EPOLLIN)) continue ; if (ep[i].data.fd == listenfd) { clilen = sizeof (cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); printf ("received from %s at PORT %d\n" , inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof (str)), ntohs(cliaddr.sin_port)); printf ("cfd %d---client %d\n" , connfd, ++num); tep.events = EPOLLIN; tep.data.fd = connfd; res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep); if (res == -1 ) perr_exit("epoll_ctl error" ); } else { sockfd = ep[i].data.fd; n = Read(sockfd, buf, MAXLINE); if (n == 0 ) { res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL ); if (res == -1 ) perr_exit("epoll_ctl error" ); Close(sockfd); printf ("client[%d] closed connection\n" , sockfd); } else if (n < 0 ) { perror("read n < 0 error: " ); res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL ); Close(sockfd); } else { for (i = 0 ; i < n; i++) buf[i] = toupper (buf[i]); Write(STDOUT_FILENO, buf, n); Writen(sockfd, buf, n); } } } } Close(listenfd); Close(efd); return 0 ; }
事件模型
EPOLL事件有两种模型:
Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
Level Triggered (LT) 水平触发只要有数据都会触发。
思考如下步骤:
假定我们已经把一个用来从管道中读取数据的文件描述符(RFD)添加到epoll描述符。
管道的另一端写入了2KB的数据
调用epoll_wait,并且它会返回RFD,说明它已经准备好读取操作
读取1KB的数据
调用epoll_wait……
在这个过程中,有两种工作模式:
ET模式
ET模式即Edge Triggered工作模式。
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。
基于非阻塞文件句柄
只有当read或者write返回EAGAIN(非阻塞读,暂时无数据)时才需要挂起、等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
LT模式
LT模式即Level Triggered工作模式。
与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,无论后面的数据是否被使用。
LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET(edge-triggered):ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).
基于管道epoll ET触发模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <stdio.h> #include <stdlib.h> #include <sys/epoll.h> #include <errno.h> #include <unistd.h> #define MAXLINE 10 int main (int argc, char *argv[]) { int efd, i; int pfd[2 ]; pid_t pid; char buf[MAXLINE], ch = 'a' ; pipe(pfd); pid = fork(); if (pid == 0 ) { close(pfd[0 ]); while (1 ) { for (i = 0 ; i < MAXLINE/2 ; i++) buf[i] = ch; buf[i-1 ] = '\n' ; ch++; for (; i < MAXLINE; i++) buf[i] = ch; buf[i-1 ] = '\n' ; ch++; write(pfd[1 ], buf, sizeof (buf)); sleep(5 ); } close(pfd[1 ]); } else if (pid > 0 ) { struct epoll_event event; struct epoll_event resevent [10]; int res, len; close(pfd[1 ]); efd = epoll_create(10 ); event.events = EPOLLIN | EPOLLET; event.data.fd = pfd[0 ]; epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0 ], &event); while (1 ) { res = epoll_wait(efd, resevent, 10 , -1 ); printf ("res %d\n" , res); if (resevent[0 ].data.fd == pfd[0 ]) { len = read(pfd[0 ], buf, MAXLINE/2 ); write(STDOUT_FILENO, buf, len); } } close(pfd[0 ]); close(efd); } else { perror("fork" ); exit (-1 ); } return 0 ; }
epoll阻塞IO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/epoll.h> #include <unistd.h> #define MAXLINE 10 #define SERV_PORT 9000 int main (void ) { struct sockaddr_in servaddr , cliaddr ; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int efd; listenfd = socket(AF_INET, SOCK_STREAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); bind(listenfd, (struct sockaddr *)&servaddr, sizeof (servaddr)); listen(listenfd, 20 ); struct epoll_event event ; struct epoll_event resevent [10]; int res, len; efd = epoll_create(10 ); event.events = EPOLLIN | EPOLLET; printf ("Accepting connections ...\n" ); cliaddr_len = sizeof (cliaddr); connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); printf ("received from %s at PORT %d\n" , inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof (str)), ntohs(cliaddr.sin_port)); event.data.fd = connfd; epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event); while (1 ) { res = epoll_wait(efd, resevent, 10 , -1 ); printf ("res %d\n" , res); if (resevent[0 ].data.fd == connfd) { len = read(connfd, buf, MAXLINE/2 ); write(STDOUT_FILENO, buf, len); } } return 0 ; }
epoll非阻塞IO
假设我们在程序中规定数据包前50个字节是对这个数据包的总述,因此我们一次读50个字节然后判断是否要完全读取这个数据包,这时候就要用到边沿性触发机制。
如果客户端只发送200Byte数据,而服务器端一次读400Byte,这时read函数是阻塞等待剩下的200Byte的,所以要设置非阻塞fd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/epoll.h> #include <unistd.h> #include <fcntl.h> #define MAXLINE 10 #define SERV_PORT 8000 int main (void ) { struct sockaddr_in servaddr , cliaddr ; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int efd, flag; listenfd = socket(AF_INET, SOCK_STREAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); bind(listenfd, (struct sockaddr *)&servaddr, sizeof (servaddr)); listen(listenfd, 20 ); struct epoll_event event ; struct epoll_event resevent [10]; int res, len; efd = epoll_create(10 ); event.events = EPOLLIN | EPOLLET; printf ("Accepting connections ...\n" ); cliaddr_len = sizeof (cliaddr); connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); printf ("received from %s at PORT %d\n" , inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof (str)), ntohs(cliaddr.sin_port)); flag = fcntl(connfd, F_GETFL); flag |= O_NONBLOCK; fcntl(connfd, F_SETFL, flag); event.data.fd = connfd; epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event); while (1 ) { printf ("epoll_wait begin\n" ); res = epoll_wait(efd, resevent, 10 , -1 ); printf ("epoll_wait end res %d\n" , res); if (resevent[0 ].data.fd == connfd) { while ((len = read(connfd, buf, MAXLINE/2 )) >0 ) write(STDOUT_FILENO, buf, len); } } return 0 ; }
epoll反应堆模型
libevent 跨平台
epoll — 服务器 — 监听 — cfd ---- 可读 ---- epoll返回 ---- read – cfd从树上摘下 — 设置监听cfd写事件, 操作
— 小写转大写 – 等待epoll_wait 返回 — 回写客户端 – cfd从树上摘下 ----- 设置监听cfd读事件, 操作 – epoll继续监听。
1 evt[i].events = EPOLLIN, evt[I].data.fd == cfd *ptr struct {int fd, void (*func)(void *arg), void *arv}
1 2 __func__ //获取当前函数名 __FILE__ //获取当前文件名
1 2 3 4 5 6 7 8 #include <stdio.h> int main(void) { printf("%s\n%s\n", __func__, __FILE__); return 0; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 #include <stdio.h> #include <sys/socket.h> #include <sys/epoll.h> #include <arpa/inet.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <stdlib.h> #include <time.h> #define MAX_EVENTS 1024 #define BUFLEN 4096 #define SERV_PORT 8080 void recvdata (int fd, int events, void *arg) ;void senddata (int fd, int events, void *arg) ;struct myevent_s { int fd; int events; void *arg; void (*call_back)(int fd, int events, void *arg); int status; char buf[BUFLEN]; int len; long last_active; }; int g_efd; struct myevent_s g_events [MAX_EVENTS +1]; void eventset (struct myevent_s *ev, int fd, void (*call_back)(int , int , void *), void *arg) { ev->fd = fd; ev->call_back = call_back; ev->events = 0 ; ev->arg = arg; ev->status = 0 ; ev->last_active = time(NULL ); return ; } void eventadd (int efd, int events, struct myevent_s *ev) { struct epoll_event epv = {0 , {0 }}; int op; epv.data.ptr = ev; epv.events = ev->events = events; if (ev->status == 1 ) { op = EPOLL_CTL_MOD; } else { op = EPOLL_CTL_ADD; ev->status = 1 ; } if (epoll_ctl(efd, op, ev->fd, &epv) < 0 ) printf ("event add failed [fd=%d], events[%d]\n" , ev->fd, events); else printf ("event add OK [fd=%d], op=%d, events[%0X]\n" , ev->fd, op, events); return ; } void eventdel (int efd, struct myevent_s *ev) { struct epoll_event epv = {0 , {0 }}; if (ev->status != 1 ) return ; epv.data.ptr = ev; ev->status = 0 ; epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv); return ; } void acceptconn (int lfd, int events, void *arg) { struct sockaddr_in cin ; socklen_t len = sizeof (cin ); int cfd, i; if ((cfd = accept(lfd, (struct sockaddr *)&cin , &len)) == -1 ) { if (errno != EAGAIN && errno != EINTR) { } printf ("%s: accept, %s\n" , __func__, strerror(errno)); return ; } do { for (i = 0 ; i < MAX_EVENTS; i++) if (g_events[i].status == 0 ) break ; if (i == MAX_EVENTS) { printf ("%s: max connect limit[%d]\n" , __func__, MAX_EVENTS); break ; } int flag = 0 ; if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0 ) { printf ("%s: fcntl nonblocking failed, %s\n" , __func__, strerror(errno)); break ; } eventset(&g_events[i], cfd, recvdata, &g_events[i]); eventadd(g_efd, EPOLLIN, &g_events[i]); } while (0 ); printf ("new connect [%s:%d][time:%ld], pos[%d]\n" , inet_ntoa(cin .sin_addr), ntohs(cin .sin_port), g_events[i].last_active, i); return ; } void recvdata (int fd, int events, void *arg) { struct myevent_s *ev = (struct myevent_s *)arg ; int len; len = recv(fd, ev->buf, sizeof (ev->buf), 0 ); eventdel(g_efd, ev); if (len > 0 ) { ev->len = len; ev->buf[len] = '\0' ; printf ("C[%d]:%s\n" , fd, ev->buf); eventset(ev, fd, senddata, ev); eventadd(g_efd, EPOLLOUT, ev); } else if (len == 0 ) { close(ev->fd); printf ("[fd=%d] pos[%ld], closed\n" , fd, ev-g_events); } else { close(ev->fd); printf ("recv[fd=%d] error[%d]:%s\n" , fd, errno, strerror(errno)); } return ; } void senddata (int fd, int events, void *arg) { struct myevent_s *ev = (struct myevent_s *)arg ; int len; len = send(fd, ev->buf, ev->len, 0 ); if (len > 0 ) { printf ("send[fd=%d], [%d]%s\n" , fd, len, ev->buf); eventdel(g_efd, ev); eventset(ev, fd, recvdata, ev); eventadd(g_efd, EPOLLIN, ev); } else { close(ev->fd); eventdel(g_efd, ev); printf ("send[fd=%d] error %s\n" , fd, strerror(errno)); } return ; } void initlistensocket (int efd, short port) { int lfd = socket(AF_INET, SOCK_STREAM, 0 ); fcntl(lfd, F_SETFL, O_NONBLOCK); eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]); eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); struct sockaddr_in sin ; memset (&sin , 0 , sizeof (sin )); sin .sin_family = AF_INET; sin .sin_addr.s_addr = INADDR_ANY; sin .sin_port = htons(port); bind(lfd, (struct sockaddr *)&sin , sizeof (sin )); listen(lfd, 20 ); return ; } int main (int argc, char *argv[]) { unsigned short port = SERV_PORT; if (argc == 2 ) port = atoi(argv[1 ]); g_efd = epoll_create(MAX_EVENTS+1 ); if (g_efd <= 0 ) printf ("create efd in %s err %s\n" , __func__, strerror(errno)); initlistensocket(g_efd, port); struct epoll_event events [MAX_EVENTS +1]; printf ("server running:port[%d]\n" , port); int checkpos = 0 , i; while (1 ) { long now = time(NULL ); for (i = 0 ; i < 100 ; i++, checkpos++) { if (checkpos == MAX_EVENTS) checkpos = 0 ; if (g_events[checkpos].status != 1 ) continue ; long duration = now - g_events[checkpos].last_active; if (duration >= 60 ) { close(g_events[checkpos].fd); printf ("[fd=%d] timeout\n" , g_events[checkpos].fd); eventdel(g_efd, &g_events[checkpos]); } } int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1 , 1000 ); if (nfd < 0 ) { printf ("epoll_wait error, exit\n" ); break ; } for (i = 0 ; i < nfd; i++) { struct myevent_s *ev = (struct myevent_s *)events [i ].data .ptr ; if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) { ev->call_back(ev->fd, events[i].events, ev->arg); } if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) { ev->call_back(ev->fd, events[i].events, ev->arg); } } } return 0 ; }
心跳包和乒乓包
心跳包:
在应用层自定义一个协议。例如服务器每隔一段时间发送一个123的数据包,客户端收到后会回一个456的数据包,当服务器收到客户端发送来的456之后就认为客户端还保持连接,如果服务器发送了123之后没有得到客户端的回应,则每隔3秒持续发送123给客户端,持续发送3次之后还没有得到回应,则认为客户端掉线了,服务端则close(cfd)且让客户端重新连接
乒乓包:
在判别网络通不通的同时还可以携带一些数据。例如:朋友圈的小圆点,客户端每隔一段时间询问有没有动态更新,服务器马上回应有或没有,如果有则回复有,客户端收到后会让小圆点变红
设置TCP属性
SO_KEEPALIVE 保持连接检测对方主机是否崩溃,避免(服务器)永远阻塞于TCP连接的输入。设置该选项后,如果2小时内在此套接口的任一方向都没有数据交换,TCP就自动给对方发一个保持存活探测分节(keepalive probe)。这是一个对方必须响应的TCP分节.它会导致以下三种情况:对方接收一切正常:以期望的ACK响应。2小时后,TCP将发出另一个探测分节。对方已崩溃且已重新启动:以RST响应。套接口的待处理错误被置为ECONNRESET,套接 口本身则被关闭。对方无任何响应:源自berkeley的TCP发送另外8个探测分节,相隔75秒一个,试图得到一个响应。在发出第一个探测分节11分钟 15秒后若仍无响应就放弃。套接口的待处理错误被置为ETIMEOUT,套接口本身则被关闭。如ICMP错误是“host unreachable(主机不可达)”,说明对方主机并没有崩溃,但是不可达,这种情况下待处理错误被置为EHOSTUNREACH。
根据上面的介绍我们可以知道对端以一种非优雅的方式断开连接的时候,我们可以设置SO_KEEPALIVE属性使得我们在2小时以后发现对方的TCP连接是否依然存在。
1 2 keepAlive = 1; setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));
如果我们不能接受如此之长的等待时间,从TCP-Keepalive-HOWTO上可以知道一共有两种方式可以设置,一种是修改内核关于网络方面的 配置参数,另外一种就是SOL_TCP字段的TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT三个选项。
The tcp_keepidle parameter specifies the interval of inactivity that causes TCP to generate a KEEPALIVE transmission for an application that requests them. tcp_keepidle defaults to 14400 (two hours).
/*开始首次KeepAlive探测前的TCP空闭时间 */
The tcp_keepintvl parameter specifies the interval between the nine retriesthat are attempted if a KEEPALIVE transmission is not acknowledged. tcp_keep ntvldefaults to 150 (75 seconds).
/* 两次KeepAlive探测间的时间间隔 */
The tcp_keepcnt option specifies the maximum number of keepalive probes tobe sent. The value of TCP_KEEPCNT is an integer value between 1 and n, where n s the value of the systemwide tcp_keepcnt parameter.
/* 判定断开前的KeepAlive探测次数*/
1 2 3 4 5 6 7 int keepIdle = 1000; int keepInterval = 10; int keepCount = 10; Setsockopt(listenfd, SOL_TCP, TCP_KEEPIDLE, (void *)&keepIdle, sizeof(keepIdle)); Setsockopt(listenfd, SOL_TCP,TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval)); Setsockopt(listenfd,SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));
SO_KEEPALIVE设置空闲2小时才发送一个“保持存活探测分节”,不能保证实时检测。对于判断网络断开时间太长,对于需要及时响应的程序不太适应。
当然也可以修改时间间隔参数,但是会影响到所有打开此选项的套接口!关联了完成端口的socket可能会忽略掉该套接字选项。
线程池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #ifndef __THREADPOOL_H_ #define __THREADPOOL_H_ typedef struct threadpool_t threadpool_t ;threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size);int threadpool_add (threadpool_t *pool, void *(*function)(void *arg), void *arg) ;int threadpool_destroy (threadpool_t *pool) ;int threadpool_all_threadnum (threadpool_t *pool) ;int threadpool_busy_threadnum (threadpool_t *pool) ;#endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <assert.h> #include <stdio.h> #include <string.h> #include <signal.h> #include <errno.h> #include "threadpool.h" #define DEFAULT_TIME 10 #define MIN_WAIT_TASK_NUM 10 #define DEFAULT_THREAD_VARY 10 #define true 1 #define false 0 typedef struct { void *(*function)(void *); void *arg; } threadpool_task_t ; struct threadpool_t { pthread_mutex_t lock; pthread_mutex_t thread_counter; pthread_cond_t queue_not_full; pthread_cond_t queue_not_empty; pthread_t *threads; pthread_t adjust_tid; threadpool_task_t *task_queue; int min_thr_num; int max_thr_num; int live_thr_num; int busy_thr_num; int wait_exit_thr_num; int queue_front; int queue_rear; int queue_size; int queue_max_size; int shutdown; }; void *threadpool_thread (void *threadpool) ;void *adjust_thread (void *threadpool) ;int is_thread_alive (pthread_t tid) ;int threadpool_free (threadpool_t *pool) ;threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size){ int i; threadpool_t *pool = NULL ; do { if ((pool = (threadpool_t *)malloc (sizeof (threadpool_t ))) == NULL ) { printf ("malloc threadpool fail" ); break ; } pool->min_thr_num = min_thr_num; pool->max_thr_num = max_thr_num; pool->busy_thr_num = 0 ; pool->live_thr_num = min_thr_num; pool->queue_size = 0 ; pool->queue_max_size = queue_max_size; pool->queue_front = 0 ; pool->queue_rear = 0 ; pool->shutdown = false ; pool->threads = (pthread_t *)malloc (sizeof (pthread_t )*max_thr_num); if (pool->threads == NULL ) { printf ("malloc threads fail" ); break ; } memset (pool->threads, 0 , sizeof (pthread_t )*max_thr_num); pool->task_queue = (threadpool_task_t *)malloc (sizeof (threadpool_task_t )*queue_max_size); if (pool->task_queue == NULL ) { printf ("malloc task_queue fail" ); break ; } if (pthread_mutex_init(&(pool->lock), NULL ) != 0 || pthread_mutex_init(&(pool->thread_counter), NULL ) != 0 || pthread_cond_init(&(pool->queue_not_empty), NULL ) != 0 || pthread_cond_init(&(pool->queue_not_full), NULL ) != 0 ) { printf ("init the lock or cond fail" ); break ; } for (i = 0 ; i < min_thr_num; i++) { pthread_create(&(pool->threads[i]), NULL , threadpool_thread, (void *)pool); printf ("start thread 0x%x...\n" , (unsigned int )pool->threads[i]); } pthread_create(&(pool->adjust_tid), NULL , adjust_thread, (void *)pool); return pool; } while (0 ); threadpool_free(pool); return NULL ; } int threadpool_add (threadpool_t *pool, void *(*function)(void *arg), void *arg) { pthread_mutex_lock(&(pool->lock)); while ((pool->queue_size == pool->queue_max_size) && (!pool->shutdown)) { pthread_cond_wait(&(pool->queue_not_full), &(pool->lock)); } if (pool->shutdown) { pthread_mutex_unlock(&(pool->lock)); } if (pool->task_queue[pool->queue_rear].arg != NULL ) { free (pool->task_queue[pool->queue_rear].arg); pool->task_queue[pool->queue_rear].arg = NULL ; } pool->task_queue[pool->queue_rear].function = function; pool->task_queue[pool->queue_rear].arg = arg; pool->queue_rear = (pool->queue_rear + 1 ) % pool->queue_max_size; pool->queue_size++; pthread_cond_signal(&(pool->queue_not_empty)); pthread_mutex_unlock(&(pool->lock)); return 0 ; } void *threadpool_thread (void *threadpool) { threadpool_t *pool = (threadpool_t *)threadpool; threadpool_task_t task; while (true ) { pthread_mutex_lock(&(pool->lock)); while ((pool->queue_size == 0 ) && (!pool->shutdown)) { printf ("thread 0x%x is waiting\n" , (unsigned int )pthread_self()); pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock)); if (pool->wait_exit_thr_num > 0 ) { pool->wait_exit_thr_num--; if (pool->live_thr_num > pool->min_thr_num) { printf ("thread 0x%x is exiting\n" , (unsigned int )pthread_self()); pool->live_thr_num--; pthread_mutex_unlock(&(pool->lock)); pthread_exit(NULL ); } } } if (pool->shutdown) { pthread_mutex_unlock(&(pool->lock)); printf ("thread 0x%x is exiting\n" , (unsigned int )pthread_self()); pthread_exit(NULL ); } task.function = pool->task_queue[pool->queue_front].function; task.arg = pool->task_queue[pool->queue_front].arg; pool->queue_front = (pool->queue_front + 1 ) % pool->queue_max_size; pool->queue_size--; pthread_cond_broadcast(&(pool->queue_not_full)); pthread_mutex_unlock(&(pool->lock)); printf ("thread 0x%x start working\n" , (unsigned int )pthread_self()); pthread_mutex_lock(&(pool->thread_counter)); pool->busy_thr_num++; pthread_mutex_unlock(&(pool->thread_counter)); (*(task.function))(task.arg); printf ("thread 0x%x end working\n" , (unsigned int )pthread_self()); pthread_mutex_lock(&(pool->thread_counter)); pool->busy_thr_num--; pthread_mutex_unlock(&(pool->thread_counter)); } pthread_exit(NULL ); } void *adjust_thread (void *threadpool) { int i; threadpool_t *pool = (threadpool_t *)threadpool; while (!pool->shutdown) { sleep(DEFAULT_TIME); pthread_mutex_lock(&(pool->lock)); int queue_size = pool->queue_size; int live_thr_num = pool->live_thr_num; pthread_mutex_unlock(&(pool->lock)); pthread_mutex_lock(&(pool->thread_counter)); int busy_thr_num = pool->busy_thr_num; pthread_mutex_unlock(&(pool->thread_counter)); if (queue_size >= MIN_WAIT_TASK_NUM && live_thr_num < pool->max_thr_num) { pthread_mutex_lock(&(pool->lock)); int add = 0 ; for (i = 0 ; i < pool->max_thr_num && add < DEFAULT_THREAD_VARY && pool->live_thr_num < pool->max_thr_num; i++) { if (pool->threads[i] == 0 || !is_thread_alive(pool->threads[i])) { pthread_create(&(pool->threads[i]), NULL , threadpool_thread, (void *)pool); add++; pool->live_thr_num++; } } pthread_mutex_unlock(&(pool->lock)); } if ((busy_thr_num * 2 ) < live_thr_num && live_thr_num > pool->min_thr_num) { pthread_mutex_lock(&(pool->lock)); pool->wait_exit_thr_num = DEFAULT_THREAD_VARY; pthread_mutex_unlock(&(pool->lock)); for (i = 0 ; i < DEFAULT_THREAD_VARY; i++) { pthread_cond_signal(&(pool->queue_not_empty)); } } } return NULL ; } int threadpool_destroy (threadpool_t *pool) { int i; if (pool == NULL ) { return -1 ; } pool->shutdown = true ; pthread_join(pool->adjust_tid, NULL ); for (i = 0 ; i < pool->live_thr_num; i++) { pthread_cond_broadcast(&(pool->queue_not_empty)); } for (i = 0 ; i < pool->live_thr_num; i++) { pthread_join(pool->threads[i], NULL ); } threadpool_free(pool); return 0 ; } int threadpool_free (threadpool_t *pool) { if (pool == NULL ) { return -1 ; } if (pool->task_queue) { free (pool->task_queue); } if (pool->threads) { free (pool->threads); pthread_mutex_lock(&(pool->lock)); pthread_mutex_destroy(&(pool->lock)); pthread_mutex_lock(&(pool->thread_counter)); pthread_mutex_destroy(&(pool->thread_counter)); pthread_cond_destroy(&(pool->queue_not_empty)); pthread_cond_destroy(&(pool->queue_not_full)); } free (pool); pool = NULL ; return 0 ; } int threadpool_all_threadnum (threadpool_t *pool) { int all_threadnum = -1 ; pthread_mutex_lock(&(pool->lock)); all_threadnum = pool->live_thr_num; pthread_mutex_unlock(&(pool->lock)); return all_threadnum; } int threadpool_busy_threadnum (threadpool_t *pool) { int busy_threadnum = -1 ; pthread_mutex_lock(&(pool->thread_counter)); busy_threadnum = pool->busy_thr_num; pthread_mutex_unlock(&(pool->thread_counter)); return busy_threadnum; } int is_thread_alive (pthread_t tid) { int kill_rc = pthread_kill(tid, 0 ); if (kill_rc == ESRCH) { return false ; } return true ; } #if 1 void *process (void *arg) { printf ("thread 0x%x working on task %d\n " ,(unsigned int )pthread_self(),*(int *)arg); sleep(1 ); printf ("task %d is end\n" ,*(int *)arg); return NULL ; } int main (void ) { threadpool_t *thp = threadpool_create(3 ,100 ,100 ); printf ("pool inited" ); int num[20 ], i; for (i = 0 ; i < 20 ; i++) { num[i]=i; printf ("add task %d\n" ,i); threadpool_add(thp, process, (void *)&num[i]); } sleep(10 ); threadpool_destroy(thp); return 0 ; } #endif
UDP服务器
无连接的不可靠报文传递
无需创建连接,所以UDP开销较小,数据传输速度快,实时性较强。
多用于对实时性要求较高的通信场合,如视频会议、电话会议等
缺点:数据传输不可靠,传输数据的正确率、传输顺序和流量都得不到控制和保证。
使用UDP协议进行数据传输,为保证数据的正确性,我们需要在应用层添加辅助校验协议来弥补UDP的不足,以达到数据可靠传输的目的。
UDP也有可能出现缓冲区被填满后,再接收数据时丢包的现象。由于它没有TCP滑动窗口的机制,通常采用如下两种方法解决:
服务器应用层设计流量控制,控制发送数据速度。
借助setsockopt函数改变接收缓冲区大小。如:
1 2 3 4 #include <sys/socket.h> int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); int n = 220x1024 setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));
C/S模型-UDP
由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,保证通讯可靠性的机制需要在应用层实现。
sendto
1 int sendto(int s, const void *buf, int len, unsigned int flags, const struct sockaddr *to, int tolen);
返回值说明:
成功则返回实际传送出去的字符数,失败返回-1,错误原因会存于errno 中。
参数说明:
s: socket描述符;
buf: UDP数据报缓存区(包含待发送数据);
len: UDP数据报的长度;
flags:调用方式标志位(一般设置为0);
to: 指向接收数据的主机地址信息的结构体(sockaddr_in需类型转换);
tolen:to所指结构体的长度;
recvfrom
1 int recvfrom(int s, void *buf, int len, unsigned int flags,struct sockaddr *from, int *fromlen);
返回值说明:
成功则返回实际接收到的字符数,失败返回-1,错误原因会存于errno 中。
参数说明:
s: socket描述符;
buf: UDP数据报缓存区(包含所接收的数据);
len: 缓冲区长度。
flags: 调用操作方式(一般设置为0)。
from: 指向发送数据的客户端地址信息的结构体(sockaddr_in需类型转换);
fromlen:指针,指向from结构体长度值。
Server.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <string.h> #include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <ctype.h> #define SERV_PORT 8000 int main (void ) { struct sockaddr_in serv_addr , clie_addr ; socklen_t clie_addr_len; int sockfd; char buf[BUFSIZ]; char str[INET_ADDRSTRLEN]; int i, n; sockfd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(SERV_PORT); bind(sockfd, (struct sockaddr *)&serv_addr, sizeof (serv_addr)); printf ("Accepting connections ...\n" ); while (1 ) { clie_addr_len = sizeof (clie_addr); n = recvfrom(sockfd, buf, BUFSIZ,0 , (struct sockaddr *)&clie_addr, &clie_addr_len); if (n == -1 ) perror("recvfrom error" ); printf ("received from %s at PORT %d\n" , inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof (str)), ntohs(clie_addr.sin_port)); for (i = 0 ; i < n; i++) buf[i] = toupper (buf[i]); n = sendto(sockfd, buf, n, 0 , (struct sockaddr *)&clie_addr, sizeof (clie_addr)); if (n == -1 ) perror("sendto error" ); } close(sockfd); return 0 ; }
Client.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <ctype.h> #define SERV_PORT 8000 int main (int argc, char *argv[]) { struct sockaddr_in servaddr ; int sockfd, n; char buf[BUFSIZ]; sockfd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1" , &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT); while (fgets(buf, BUFSIZ, stdin ) != NULL ) { n = sendto(sockfd, buf, strlen (buf), 0 , (struct sockaddr *)&servaddr, sizeof (servaddr)); if (n == -1 ) perror("sendto error" ); n = recvfrom(sockfd, buf, BUFSIZ, 0 , NULL , 0 ); if (n == -1 ) perror("recvfrom error" ); write(STDOUT_FILENO, buf, n); } close(sockfd); return 0 ; }
UDP实现广播
IP:192.168.42.255(广播)
IP:192.168.42.1(网关)
广播需要设置套接字开启广播功能
int flag = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &flag, sizeof(flag));
给sockfd开放广播权限。
客户端需要绑定端口号
server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/socket.h> #include <string.h> #include <arpa/inet.h> #include <net/if.h> #define SERVER_PORT 8000 #define MAXLINE 1500 #define BROADCAST_IP "192.168.42.255" #define CLIENT_PORT 9000 int main (void ) { int sockfd; struct sockaddr_in serveraddr , clientaddr ; char buf[MAXLINE]; sockfd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&serveraddr, sizeof (serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(SERVER_PORT); bind(sockfd, (struct sockaddr *)&serveraddr, sizeof (serveraddr)); int flag = 1 ; setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &flag, sizeof (flag)); bzero(&clientaddr, sizeof (clientaddr)); clientaddr.sin_family = AF_INET; inet_pton(AF_INET, BROADCAST_IP, &clientaddr.sin_addr.s_addr); clientaddr.sin_port = htons(CLIENT_PORT); int i = 0 ; while (1 ) { sprintf (buf, "Drink %d glasses of water\n" , i++); sendto(sockfd, buf, strlen (buf), 0 , (struct sockaddr *)&clientaddr, sizeof (clientaddr)); sleep(1 ); } close(sockfd); return 0 ; }
Client
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #define SERVER_PORT 8000 #define MAXLINE 4096 #define CLIENT_PORT 9000 int main (int argc, char *argv[]) { struct sockaddr_in localaddr ; int confd; ssize_t len; char buf[MAXLINE]; confd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&localaddr, sizeof (localaddr)); localaddr.sin_family = AF_INET; inet_pton(AF_INET, "0.0.0.0" , &localaddr.sin_addr.s_addr); localaddr.sin_port = htons(CLIENT_PORT); int ret = bind(confd, (struct sockaddr *)&localaddr, sizeof (localaddr)); if (ret == 0 ) printf ("...bind ok...\n" ); while (1 ) { len = recvfrom(confd, buf, sizeof (buf), 0 , NULL , 0 ); write(STDOUT_FILENO, buf, len); } close(confd); return 0 ; }
组播
广播是把数据报发送给所有的机器,容易造成广播风暴,所以推荐使用组播
组播组可以是永久的也可以是临时的。组播组地址中,有一部分由官方分配的,称为永久组播组。永久组播组保持不变的是它的ip地址,组中的成员构成可以发生变化。永久组播组中成员的数量都可以是任意的,甚至可以为零。那些没有保留下来供永久组播组使用的ip组播地址,可以被临时组播组利用。
1 2 3 4 224.0.0.0~224.0.0.255 为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用; 224.0.1.0~224.0.1.255 是公用组播地址,可以用于Internet;欲使用需申请。 224.0.2.0~238.255.255.255 为用户可用的组播地址(临时组地址),全网范围内有效; 239.0.0.0~239.255.255.255 为本地管理组播地址,仅在特定的本地范围内有效。(局域网使用)
可使用ip ad命令查看网卡编号,如:
1 2 3 4 5 6 7 8 9 10 11 itcast$ ip ad 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000 link/ether 00:0c:29:0a:c4:f4 brd ff:ff:ff:ff:ff:ff inet6 fe80::20c:29ff:fe0a:c4f4/64 scope link valid_lft forever preferred_lft forever
eth0网卡的编号是2
if_nametoindex函数可以根据网卡名,获取网卡序号。
1 unsigned int if_nametoindex(const char *ifname); //传递网卡名字
Server.c
服务端要开启组播权限
1 2 3 4 5 6 7 #define GROUP "239.0.0.2" struct ip_mreqn group; inet_pton(AF_INET, GROUP, &group.imr_multiaddr); /* 设置组地址 */ inet_pton(AF_INET, "0.0.0.0", &group.imr_address); /* 本地任意IP */ group.imr_ifindex = if_nametoindex("eth0"); /* 给出网卡名,转换为对应编号: eth0 --> 编号 命令:ip ad */ setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group, sizeof(group)); /* 组播权限 */
查看ip_mreqn结构体
1 2 3 4 5 6 7 sudo grep -r "ip_mreqn" /usr/ -n struct ip_mreqn { struct in_addr imr_multiaddr; /* IP multicast address of group 组地址*/ struct in_addr imr_address; /* local IP address of interface 本地ip*/ int imr_ifindex; /* Interface index 网卡编号*/ };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <net/if.h> #define SERVER_PORT 8000 #define CLIENT_PORT 9000 #define MAXLINE 1500 #define GROUP "239.0.0.2" int main (void ) { int sockfd; struct sockaddr_in serveraddr , clientaddr ; char buf[MAXLINE] = "itcast\n" ; struct ip_mreqn group ; sockfd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&serveraddr, sizeof (serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(SERVER_PORT); bind(sockfd, (struct sockaddr *)&serveraddr, sizeof (serveraddr)); inet_pton(AF_INET, GROUP, &group.imr_multiaddr); inet_pton(AF_INET, "0.0.0.0" , &group.imr_address); group.imr_ifindex = if_nametoindex("eth0" ); setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group, sizeof (group)); bzero(&clientaddr, sizeof (clientaddr)); clientaddr.sin_family = AF_INET; inet_pton(AF_INET, GROUP, &clientaddr.sin_addr.s_addr); clientaddr.sin_port = htons(CLIENT_PORT); int i = 0 ; while (1 ) { sprintf (buf, "itcast %d\n" , i++); sendto(sockfd, buf, strlen (buf), 0 , (struct sockaddr *)&clientaddr, sizeof (clientaddr)); sleep(1 ); } close(sockfd); return 0 ; }
client.c
客户端要加入组播
1 2 3 4 5 6 7 #define GROUP "239.0.0.2" inet_pton(AF_INET, GROUP, &group.imr_multiaddr); /* 设置组地址 */ inet_pton(AF_INET, "0.0.0.0", &group.imr_address); /* 使用本地任意IP添加到组播组 */ group.imr_ifindex = if_nametoindex("eth0"); /* 通过网卡名-->编号 ip ad */ setsockopt(confd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group));/* 设置client 加入多播组 */
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <net/if.h> #define SERVER_PORT 8000 #define CLIENT_PORT 9000 #define GROUP "239.0.0.2" int main (int argc, char *argv[]) { struct sockaddr_in localaddr ; int confd; ssize_t len; char buf[BUFSIZ]; struct ip_mreqn group ; confd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&localaddr, sizeof (localaddr)); localaddr.sin_family = AF_INET; inet_pton(AF_INET, "0.0.0.0" , &localaddr.sin_addr.s_addr); localaddr.sin_port = htons(CLIENT_PORT); bind(confd, (struct sockaddr *)&localaddr, sizeof (localaddr)); inet_pton(AF_INET, GROUP, &group.imr_multiaddr); inet_pton(AF_INET, "0.0.0.0" , &group.imr_address); group.imr_ifindex = if_nametoindex("eth0" ); setsockopt(confd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof (group)); while (1 ) { len = recvfrom(confd, buf, sizeof (buf), 0 , NULL , 0 ); write(STDOUT_FILENO, buf, len); } close(confd); return 0 ; }
socket IPC(本地套接字domain)
使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。
对比网络套接字地址结构和本地套接字地址结构:
1 2 3 4 5 6 7 8 9 10 struct sockaddr_in { __kernel_sa_family_t sin_family; /* Address family */ 地址结构类型 __be16 sin_port; /* Port number */ 端口号 struct in_addr sin_addr; /* Internet address */ IP地址 }; struct sockaddr_un { __kernel_sa_family_t sun_family; /* AF_UNIX */ 地址结构类型 char sun_path[UNIX_PATH_MAX]; /* pathname */ socket文件名(含路径) };
以下程序将UNIX Domain socket绑定到一个地址。
1 2 size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path); #define offsetof(type, member) ((int)&((type *)0)->MEMBER)
offsetof
unlink
删除pathname指定的硬链接,并由pathname所引用的文件链接计数减1
1 2 3 #include <unistd.h> int unlink(const char *pathname);
server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <strings.h> #include <string.h> #include <ctype.h> #include <arpa/inet.h> #include <sys/un.h> #include <stddef.h> #include "wrap.h" #define SERV_ADDR "serv.socket" int main (void ) { int lfd, cfd, len, size, i; struct sockaddr_un servaddr , cliaddr ; char buf[4096 ]; lfd = Socket(AF_UNIX, SOCK_STREAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servaddr.sun_family = AF_UNIX; strcpy (servaddr.sun_path,SERV_ADDR); len = offsetof(struct sockaddr_un, sun_path) + strlen (servaddr.sun_path); unlink(SERV_ADDR); Bind(lfd, (struct sockaddr *)&servaddr, len); Listen(lfd, 20 ); printf ("Accept ...\n" ); while (1 ) { len = sizeof (cliaddr); cfd = Accept(lfd, (struct sockaddr *)&cliaddr, (socklen_t *)&len); len -= offsetof(struct sockaddr_un, sun_path); cliaddr.sun_path[len] = '\0' ; printf ("client bind filename %s\n" , cliaddr.sun_path); while ((size = read(cfd, buf, sizeof (buf))) > 0 ) { for (i = 0 ; i < size; i++) buf[i] = toupper (buf[i]); write(cfd, buf, size); } close(cfd); } close(lfd); return 0 ; }
Client
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <strings.h> #include <string.h> #include <ctype.h> #include <arpa/inet.h> #include <sys/un.h> #include <stddef.h> #include "wrap.h" #define SERV_ADDR "serv.socket" #define CLIE_ADDR "clie.socket" int main (void ) { int cfd, len; struct sockaddr_un servaddr , cliaddr ; char buf[4096 ]; cfd = Socket(AF_UNIX, SOCK_STREAM, 0 ); bzero(&cliaddr, sizeof (cliaddr)); cliaddr.sun_family = AF_UNIX; strcpy (cliaddr.sun_path,CLIE_ADDR); len = offsetof(struct sockaddr_un, sun_path) + strlen (cliaddr.sun_path); unlink(CLIE_ADDR); Bind(cfd, (struct sockaddr *)&cliaddr, len); bzero(&servaddr, sizeof (servaddr)); servaddr.sun_family = AF_UNIX; strcpy (servaddr.sun_path,SERV_ADDR); len = offsetof(struct sockaddr_un, sun_path) + strlen (servaddr.sun_path); Connect(cfd, (struct sockaddr *)&servaddr, len); while (fgets(buf, sizeof (buf), stdin ) != NULL ) { write(cfd, buf, strlen (buf)); len = read(cfd, buf, sizeof (buf)); write(STDOUT_FILENO, buf, len); } close(cfd); return 0 ; }