linux网络编程

网络基础

网络术语概念

网卡

在这里插入图片描述

又称为网络适配器或网络接口卡NIC,但是现在更多的人愿意使用更为简单的名称“网卡” 通过网卡能够使不同的计算机之间连接,从而完成数据通信等功能

mac地址

在这里插入图片描述

MAC地址,用于标识网络设备,类似于身份证号,且理论上全球唯一。 以太网内的MAC地址是一个48bit的值。

IP地址

在这里插入图片描述

IP地址是一种Internet上的主机编址方式,也称为网际协议地址 。由32bit组成,分为{子网ID,主机ID}两部分。
子网ID:IP地址中由子网掩码中1覆盖的连续位。
主机ID:IP地址中由子网掩码中0覆盖的连续位

特点:
1)子网ID不同的网络不能直接通信,如果要通信则需要路由器转发 。
2)主机ID全为0的IP地址表示网段地址,主机ID全为1的IP地址表示该网段的广播地址。
3)通常 127.0.0.1 称为回环地址,能ping通127.0.0.1说明本机的网卡和IP协议安装都没有问题。

子网掩码

子网掩码(subnet mask)又叫网络掩码、地址掩码,是一个32bit由1和0组成的数值,并且1和0分别连续,其作用是指明IP地址中哪些位标识的是主机所在的子网以及哪些位标识的是主机号。必须结合IP地址使用,IP地址中由子网掩码中1覆盖的连续位为子网ID,其余为主机ID。
子网掩码的表现形式:
192.168.220.0/255.255.255.0
192.168.220.0/24

端口

TCP/IP协议采用端口标识通信的进程 ,一个进程拥有一个端口后,传输层送到该端口的数据全部被该进程接收,同样,进程送交传输层的数据也通过该端口被送出 。在网络程序中,用端口号来标识一个运行的网络程序。

网络应用程序,至少要占用一个端口号,也可以占有多个端口号 。1至1023是知名端口,由互联网数字分配机构(IANA)根据用户需要进行统一分配,例如,http 80 tcp、ftp 21 tcp、tftp 69 udp。

OSI七层模型和TCP/IP四层模型

在这里插入图片描述

  1. 物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
  2. 数据链路层:定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、1
  3. 网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。IP协议就是网络层的。
  4. 传输层:定义了一些传输数据的协议和端口号,主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。
  5. 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。
  6. 表示层:可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。
  7. 应用层:这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。

一般在应用开发过程中,讨论最多的是TCP/IP模型:
在这里插入图片描述

路由

路由(名词),数据包从源地址到目的地址所经过的路径,由一系列路由节点组成。
路由(动词),某个路由节点为数据包选择投递方向的选路过程。

路由器,是连接因特网中各局域网、广域网的设备,它会根据信道的情况自动选择和设定路由,以最佳路径,按前后顺序发送信号的设备。传统地,路由器工作于OSI七层协议中的第三层,其主要任务是接收来自一个网络接口的数据包,根据其中所含的目的地址,决定转发到下一个目的地址。路由器首先得在转发路由表中查找它的目的地址,若找到了目的地址,就在数据包的帧格前添加下一个MAC地址,同时IP数据包头的TTL(Time To Live)域也开始减数, 并重新计算校验和。当数据包被送到输出端口时,它需要按顺序等待,以便被传送到输出链路上。

路由表(Routing Table),在计算机网络中,路由表或称路由择域信息库(RIB)是一个存储在路由器或者联网计算机中的电子表格(文件)或类数据库。路由表存储着指向特定网络地址的路径。
路由表中的一行,每个条目主要由目的网络地址、子网掩码、下一跳地址、发送接口四部分组成,如果要发送的数据包的目的网络地址匹配路由表中的某一行,就按规定的接口发送到下一跳地址。
路由表中的最后一行,主要由下一跳地址和发送接口两部分组成,当目的地址与路由表中其它行都不匹配时,就按缺省路由条目规定的接口发送到下一跳地址。

交换机

以太网交换机是基于以太网传输数据的交换机,以太网采用共享总线型传输媒体方式的局域网。以太网交换机的结构是每个端口都直接与主机相连,并且一般都工作在全双工方式。交换机能同时连通许多对端口,使每一对相互通信的主机都能像独占通信媒体那样,进行无冲突地传输数据。
以太网交换机工作于OSI网络参考模型的第二层(即数据链路层),是一种基于MAC(Media Access Control,介质访问控制)地址识别、完成以太网数据帧转发的网络设备。

hub集线器

集线器实际上就是中继器的一种,其区别仅在于集线器能够提供更多的端口服务,所以集线器又叫多口中继器。
集线器功能是随机选出某一端口的设备,并让它独占全部带宽,与集线器的上联设备(交换机、路由器或服务器等)进行通信。从Hub的工作方式可以看出,它在网络中只起到信号放大和重发作用,其目的是扩大网络的传输范围,而不具备信号的定向传送能力,是—个标准的共享式设备。其次是Hub只与它的上联设备(如上层Hub、交换机或服务器)进行通信,同层的各端口之间不会直接进行通信,而是通过上联设备再将信息广播到所有端口上。 由此可见,即使是在同一Hub的不同两个端口之间进行通信,都必须要经过两步操作:
第一步是将信息上传到上联设备;
第二步是上联设备再将该信息广播到所有端口上。

DNS服务器

DNS 是域名系统 (Domain Name System) 的缩写,是因特网的一项核心服务,它作为可以将域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP地址串。
它是由解析器以及域名服务器组成的。域名服务器是指保存有该网络中所有主机的域名和对应IP地址,并具有将域名转换为IP地址功能的服务器。

MTU

MTU:通信术语 最大传输单元(Maximum Transmission Unit,MTU)。
是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡、串口等)。

以下是一些协议的MTU:
FDDI协议:4352字节
以太网(Ethernet)协议:1500字节
PPPoE(ADSL)协议:1492字节
X.25协议(Dial Up/Modem):576字节
Point-to-Point:4470字节

协议

从应用的角度出发,协议可理解为“规则”,是数据传输和数据的解释的规则。

网络接口层协议(链路层)

ARP协议是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。

RARP是反向地址转换协议,通过MAC地址确定IP地址。

网络层协议

IP协议是因特网互联协议(Internet Protocol)。

ICMP协议是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。

IGMP协议是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。

传输层协议

TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。

应用层协议

HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议。

FTP文件传输协议(File Transfer Protocol)。

协议数据包封装

应用程序对通讯数据的含义进行解释,而传输层及其以下处理通讯的细节,将数据从一台计算机通过一定的路径发送到另一台计算机。应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation),如下图所示:
在这里插入图片描述
不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报(datagram),在链路层叫做帧(frame)。数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理。

各个协议包头的大小:
IP协议,20~60字节;
UDP协议,8字节;
TCP协议,20~60字节;

以太网帧数据格式

在这里插入图片描述
其中的源地址和目的地址是指网卡的硬件地址(也叫MAC地址),长度是48位。协议字段有三种值,分别对应IP、ARP、RARP。帧尾是CRC校验码。
网帧中的数据长度规定最小46字节,最大1500字节,ARP和RARP数据包的长度不够46字节,要在后面补填充位。最大值1500称为以太网的最大传输单元(MTU),不同的网络类型有不同的MTU,如果一个数据包从以太网路由到拨号链路上,数据包长度大于拨号链路的MTU,则需要对数据包进行分片(fragmentation)。
注意,MTU这个概念指数据帧中有效载荷的最大长度,不包括帧头长度。

其他数据报格式

ARP数据报格式

在这里插入图片描述

IP数据报格式
在这里插入图片描述

UDP数据报格式
在这里插入图片描述

tcp协议

tcp协议数据报格式

在这里插入图片描述
4位首部表示TCP协议头的长度,以4字节为单位,因此TCP协议头最长可以是4x15=60字节,如果没有选项字段,TCP协议头最短20字节。16位检验和将TCP协议头和数据都计算在内。
RST:拒绝通信;
SYN/FIN:发起连接/关闭连接;

tcp通信时序

下图是一次TCP通讯的时序图。TCP连接建立断开。包含大家熟知的三次握手和四次握手。
在这里插入图片描述
在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。
双方发送的段按时间顺序编号为1-10,各段中的主要信息在箭头上标出,例如段2的箭头上标着SYN, 8000(0), ACK1001, ,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是1001,带有一个mss(Maximum Segment Size,最大报文长度)选项值为1024

三次握手

  1. 客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的段1(规定SYN位和FIN位也要占一个序号)。
  2. 服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。它表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。
  3. 客户必须再次回应服务器端一个ACK报文,这是报文段3。

注意:
在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。

四次挥手

  1. 客户端发出段7,FIN位表示关闭连接的请求。
  2. 服务器发出段8,应答客户端的关闭连接请求。
  3. 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
  4. 客户端发出段10,应答服务器的关闭连接请求。

建立连接的过程是三方握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。

tcp流量控制(滑动窗口)

如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。TCP协议通过“滑动窗口(Sliding Window)”机制解决这一问题。看下图的通讯过程:
在这里插入图片描述

  1. 发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
  2. 发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
  3. 接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
  4. 接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
  5. 发送端发出段12-13,每个段带2K数据,段13同时还包含FIN位。
  6. 接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。
  7. 接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
  8. 接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。
  9. 接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。

上图在接收端用小方块表示1K数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序提走数据,虚线框是向右滑动的,因此称为滑动窗口。

从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。

tcp状态转换

center
注意:粗实线表示主动方,虚线表示被动方

状态含义
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_1FIN_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) TIME_WAIT状态的存在有两个理由:
(1)让4次握手关闭流程更加可靠;4次握手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。
(2)防止lost duplicate对后续新建正常链接的传输造成破坏(上一次的数据包被本次连接使用了)。

端口复用
使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
在server代码的socket()和bind()调用之间插入如下代码:

int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

Socket编程

在这里插入图片描述
在TCP/IP协议中,IP地址+端口号唯一标识网络通讯中的一个进程。IP地址+端口号就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。

网络字节序和IP转换接口

  1. 网络字节序转换
    将主机字节序转换成网络字节序

    // TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
    uint32_t htonl(uint32_t hostlong);
    uint16_t htons(uint16_t hostshort);
    

    将网络字节序转换成主机字节序

    uint32_t ntohl(uint32_t netlong);
    uint16_t ntohs(uint16_t netshort);
    
  2. ip地址转换(ipv4和ipv6都适用)
    将点分十进制转换成用于网络传输的数值格式

    int inet_pton(int af, const char *src, void *dst);
    

    将数字格式转换成点分十进制的格式

    const char *inet_ntop(int af, const void *src, char *dst, 
    socklen_t size);
    

sockaddr 数据结构

strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
在这里插入图片描述
注意:上图中各个结构的实际长度和图中方块的长度是不成比例的,sockaddr_un的方块应该是最长的,图中没有体现出来,勿纠结。

//早期
struct sockaddr {
	sa_family_t sa_family; 		/* address family, AF_xxx */
	char sa_data[14];			/* 14 bytes of protocol address */
};

//ipv4地址
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;
};

//ipv6地址
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) */
};
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
};

//Unix Domain Socket本地套接字
#define UNIX_PATH_MAX 108
	struct sockaddr_un {
	__kernel_sa_family_t sun_family; 	/* AF_UNIX */
	char sun_path[UNIX_PATH_MAX]; 	/* pathname */
};


Socket API

示例程序

TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。
在这里插入图片描述

socket函数
/*
* socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上* 收发数据,如果socket()调用出错则返回-1。
*/
#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

bind函数
/*
* bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。
* struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,
* 所以需要第三个参数addrlen指定结构体的长度。
*/
#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
listen函数
/*
* 典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接;
* 如果有大量的客户端发起连接而服务器来不及处理,则这些客户端处于连接等待状态;
* listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
*/
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
  • sockfd:
    socket文件描述符
  • backlog:
    排队建立3次握手队列和刚刚建立3次握手队列的链接数和
  • 返回值:
    成功返回0,失败返回-1, 设置errno

查看系统默认backlog
cat /proc/sys/net/ipv4/tcp_max_syn_backlog

accept函数
/*
* 三方握手完成后,服务器调用accept()接受连接,
* 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
*/
#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
connect 函数
/*
* 客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,
* 而connect的参数是对方的地址。
*/
#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
getsockname和getpeername函数
/*
 * 这两个函数获取与某个套接字关联的本地协议地址或者连接对端协议地址
 */
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr* peeraddr, socklen_t *addrlen);

TCP协议通信流程

下图是基于TCP协议的客户端/服务器程序的一般流程:
在这里插入图片描述

UDP协议通信流程

在这里插入图片描述
由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,保证通讯可靠性的机制需要在应用层实现。

本地套接字(UNIX Domain Socket)

UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。
对比网络套接字地址结构和本地套接字地址结构:

//网络套接字
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的过程和网络socket十分相似,注意的是,一般客户端也需要使用bind()调用绑定一个的地址。

IO复用

多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
主要使用的方法有三种。

select

  1. select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
  2. 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力。
#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

poll

相较于select而言,poll的优势:
(1) 传入、传出事件分离。无需每次调用时,重新设定监听事件。
(2)文件描述符上限,可突破1024限制。能监控的最大上限数可使用配置文件调整。


#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。

epoll

epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

目前epoll是linux大规模并发网络程序中的热门首选模型。

epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

1)创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。

#include <sys/epoll.h>
int epoll_create(int size);		
		@size:监听数目(内核参考值)。从Linux 2.6.8开始,max_size参数将被忽略,但必须大于零。
		@返回值:成功:非负文件描述符;失败:-1,设置相应的errno

2)控制某个epoll监控的文件描述符上的事件:注册、修改、删除。

#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队列里
		
		@返回值:成功:0;失败:-1,设置相应的errno

3)等待所监控文件描述符上有事件的产生,类似于select()调用。

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
		@events:		用来存内核得到事件的集合,可简单看作数组。
		@maxevents:	告之内核这个events有多大(表示本次可以返回的最大事件数目,
						通常maxevents参数与预分配的events数组的大小是相等的),
						注意maxevents的值不能大于创建epoll_create()时的size。
		@timeout:	是超时时间
			-1:	阻塞
			0:	立即返回,非阻塞
			>0:	指定毫秒
		@返回值:	成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1

epoll事件模型

EPOLL事件有两种模型:
(1)Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
(2)Level Triggered (LT) 水平触发只要有数据都会触发。

思考如下步骤:

  1. 假定我们已经把一个用来从管道中读取数据的文件描述符(rfd)添加到epoll描述符。
  2. 管道的另一端写入了2KB的数据
  3. 调用epoll_wait,并且它会返回rfd,说明它已经准备好读取操作
  4. 读取1KB的数据
  5. 调用epoll_wait……

在这个过程中,有两种工作模式:

  • ET模式
    如果我们在第1步将rfd添加到epoll描述符的时候使用了EPOLLET标志,在第5步的时候,调用者可能会放弃读取仍存在于文件输入缓冲区内的剩余数据(如果一次性没有读完)。因为只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。
    epoll工作在ET模式的时候,必须使用非阻塞套接口读取数据,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死(下面有解释)。最好以下面的方式调用ET模式的epoll接口。

    • 基于非阻塞文件句柄
    • 只有当read或者write返回EAGAIN(非阻塞读,暂时无数据)时才需要挂起、等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
  • LT模式(缺省工作方式)
    与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,会一直触发直接数据被处理完。

两者的比较:

LT(level triggered)ET(edge-triggered)
LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。

总的来说LT模式可以不用循环去读取数据,而ET模式下必须循环去读取完数据。

为什么ET模式下要使用非阻塞?
是因为ET模式只在socket描述符状态发生变化时才触发事件,如果不一次把socket内核缓冲区的数据读完,会导致socket内核缓冲区中即使还有一部分数据,该socket的可读事件也不会被触发。因此一般使用下面这行代码循环读数据。

while( (len=recv(fd,buf,sizeof(buf),0)) > 0 ){}

如果文件IO设置为阻塞,则必然会阻塞在这里,造成没办法重新获取epoll_wait()事件。因此ET模式下文件IO一定要设置为非阻塞。

epoll和select、poll的区别

  1. select和poll将文件描述符集合维护在用户空间,每次使用都需要将整个集合拷贝到内核空间。而epoll将文件描述符集合维护在内核空间,每次增删文件描述符都需要使用系统调用。如果短时间内有大量新的请求连接上来,epoll的性能可能不如上述二者。
  2. select使用线性表来维护文件描述符集合,数量有限制;poll使用链表来维护文件描述符集合;epoll则使用红黑树来维护文件描述符集合,同时使用双向链表来保存就绪的事件。
  3. select/poll的主要开销来自内核判断是否有文件描述符就绪的过程,每次执行select/poll调用时,会采用遍历整个集合的方法来判断是否有文件描述符就绪;epoll则不需要,当有事件发生时,会自动触发epoll回调函数通知epoll文件描述符,然后内核将这些就绪的描述符放到双向链表中,等待epoll_wait的调用处理。
  4. 当监测的fd数量较小,且各个fd都很活跃的情况下,建议使用select/poll;当监听的fd数量较多,且单位时间仅部分fd活跃的情况下,使用epoll的性能会很好。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值