Linux_网络编程

网络编程
TCP/IP 四层协议
在这里插入图片描述
OSI七层协议模型 (open system interconnection)
应用层————为应用数据提供服务
表示层————数据格式转化,数据加密
会话层————建立、维护和管理会话
传输层————建立、维护和管理端到端的链接,控制数据传输的方式
网络层————数据传输线路选择,IP地址及路由选择
数据链路层———物理通路的发送和数据包的划分,附加Mac地址到数据包
物理层———01比特流的转换
数据传输由顶向下,下层为上层提供服务

TCP/IP四层协议模型
应用层———负责处理特定的应用程序细节, 如ftp,http ,smtp,ssh 等
运输层———主要为两台主机上的应用提供端到端的通信, 如TCP,UDP。
网络层(互联网层)———处理分组在网络中的活动,比如分组的选路。
链路层(数据链路层/网络接口层)———包括操作系统中的设备驱动程序、计算机中对应的 网络接口卡,01比特流的转换

协议封装
下层协议通过封装为上层协议提供服务。应用程序数据在发送到物理网络上之前,将沿着协议栈从上往下依次传递。每层协议都将在上层数据的基础上加上自己的头部信息(有时也包括尾部信息),以实现该层的功能。
在这里插入图片描述
TCP 协议头部
在这里插入图片描述
源端口号和目的端口号:再加上Ip首部的源IP地址和目的IP地址可以唯一确定一个TCP连接
数据序号:表示在这个报文段中的第一个数据字节序号
确认序号:仅当ACK标志为1时有效。确认号表示期望收到的下一个字节的序号(这个下面再详细分析)
偏移:就是头部长度,有4位,跟IP头部一样,以4字节为单位。最大是60个字节
保留位:6位,必须为0
6个标志位:
URG-紧急指针有效
ACK-确认序号有效
PSH-接收方应尽快将这个报文交给应用层
RST-连接重置
SYN-同步序号用来发起一个连接
FIN-终止一个连接
窗口字段:16位,代表的是窗口的字节容量,也就是TCP的标准窗口最大为2^16 - 1 = 65535个字节
校验和:源机器基于数据内容计算一个数值,收信息机要与源机器数值 结果完全一样,从而证明数据的有效性。检验和覆盖了整个的TCP报文段:这是一个强制性的字段,一定是由发送端计算和存储,并由接收端进行验证的。
紧急指针:是一个正偏移量,与序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式
选项与填充(必须为4字节整数倍,不够补0):
最常见的可选字段的最长报文大小MSS(Maximum Segment Size),每个连接方通常都在一个报文段中指明这个选项。它指明本端所能接收的最大长度的报文段。
该选项如果不设置,默认为536(20+20+536=576字节的IP数据报)

三次握手
(1)男孩喜欢女孩,于是写了一封信告诉女孩:我喜欢你,请和我交往吧!写完信之后,男孩焦急地等待,因为不知道信能否顺利传达给女孩。
(2)女孩收到男孩的情书后,心花怒放,原来我们是两情相悦呀!于是给男孩写了一封回信:我收到你的情书了,也明白了你的心意,其实,我也喜欢你!我愿意和你交往!;
写完信之后,女孩也焦急地等待,因为不知道回信能否能顺利传达给男孩。
(3)男孩收到回信之后很开心,因为发出的情书女孩收到了,并且从回信中知道了女孩喜欢自己,并且愿意和自己交往。然后男孩又写了一封信告诉女孩:你的心意和信我都收到了,谢谢你,还有我爱你!
女孩收到男孩的回信之后,也很开心,因为发出的情书男孩收到了。由此男孩女孩双方都知道了彼此的心意,之后就快乐地交流起来了~~

所谓的三次握手即TCP连接的建立。这个连接必须是一方主动打开,另一方被动打开的。

在这里插入图片描述
(1)首先客户端向服务器端发送一段TCP报文,其中:
标记位为SYN,表示“请求建立新连接”;序号为Seq=X(X一般为1);随后客户端进入SYN-SENT阶段。

(2)服务器端接收到来自客户端的TCP报文之后,结束LISTEN阶段。并返回一段TCP报文,其中:
标志位为SYN和ACK,表示“确认客户端的报文Seq序号有效,服务器能正常接收客户端发送的数据,并同意创建新连接”(即告诉客户端,服务器收到了你的数据);序号为Seq=y;确认号为Ack=x+1,表示收到客户端的序号Seq并将其值加1作为自己确认号Ack的值;随后服务器端进入SYN-RCVD阶段。

(3)客户端接收到来自服务器端的确认收到数据的TCP报文之后,明确了从客户端到服务器的数据传输是正常的,结束SYN-SENT阶段。并返回最后一段TCP报文。其中:
标志位为ACK,表示“确认收到服务器端同意连接的信号”(即告诉服务器,我知道你收到我发的数据了);序号为Seq=x+1,表示收到服务器端的确认号Ack,并将其值作为自己的序号值;确认号为Ack=y+1,表示收到服务器端序号Seq,并将其值加1作为自己的确认号Ack的值;随后客户端进入ESTABLISHED阶段。服务器收到来自客户端的“确认收到服务器数据”的TCP报文之后,明确了从服务器到客户端的数据传输是正常的。结束SYN-SENT阶段,进入ESTABLISHED阶段。
在客户端与服务器端传输的TCP报文中,双方的确认号Ack和序号Seq的值,都是在彼此Ack和Seq值的基础上进行计算的,这样做保证了TCP报文传输的连贯性。一旦出现某一方发出的TCP报文丢失,便无法继续"握手",以此确保了"三次握手"的顺利完成。

滑动窗口
维持发送方/接收方缓冲区,缓冲区是用来解决网络之间数据不可靠的问题,例如丢包,重复包,出错,乱序。在TCP协议中,发送方和接受方通过各自维护自己的缓冲区。通过商定包的重传机制等一系列操作,来解决不可靠的问题。

问题一:如果你是TCP设计者,如何保证数据包依次序传输?
解决方案: 发送 <=> 确认机制
在这里插入图片描述
问题二: 采用问题一的解决方案会带来效率上的弊端,数据包在网络上的传输需要时间
解决方案: 一次发送多个包,同时确认多个
在这里插入图片描述
问题三: 我们每次需要发多少个包过去呢?发送多少包是最优解呢?

正常情况
我们能不能把第一个和第二个包发过去后,收到第一个确认包就把第三个包发过去呢?而不
是去等到第二个包的确认包才去发第三个包。这样就很自然的产生了我们"滑动窗口"的实
现。
在这里插入图片描述
在图中,我们可看出灰色1号2号3号包已经发送完毕,并且已经收到Ack。这些包就已经
是过去式。4、5、6、7号包是黄色的,表示已经发送了。但是并没有收到对方的Ack,所以
也不知道接收方有没有收到。8、9、10号包是绿色的。是我们还没有发送的。这些绿色也
就是我们接下来马上要发送的包。 可以看出我们的窗口正好是7格。后面的11-16还没有
被读进内存。要等4号-10号包有接下来的动作后,我们的包才会继续往下发送。
在这里插入图片描述
可以看到4号包对方已经被接收到,所以被涂成了灰色。“窗口”就往右移一格,这里只要
保证“窗口”是7格的。 我们就把11号包读进了我们的缓存。进入了“待发送”的状态。
8、9号包已经变成了黄色,表示已经发送出去了。接下来的操作就是一样的了,确认包后,
窗口往后移继续将未发送的包读进缓存,把“待发送“状态的包变为”已发送“。

丢包情况
有可能我们包发过去,对方的Ack丢了。也有可能我们的包并没有发送过去。从发送方角度
看就是我们没有收到Ack。
在这里插入图片描述
一般情况:一直在等Ack。如果一直等不到的话,我们也会把读进缓存的待发送的包也一
起发过去。但是,这个时候我们的窗口已经发满了。所以并不能把12号包读进来,而是始
终在等待5号包的Ack。
在这里插入图片描述
一般情况:一直在等Ack。如果一直等不到的话,我们也会把读进缓存的待发送的包也一
起发过去。但是,这个时候我们的窗口已经发满了。所以并不能把12号包读进来,而是始
终在等待5号包的Ack。

如果我们这个Ack始终不来怎么办呢? 采用超时重传机制解决:
发送端每发送一个报文段,就启动一个定时器并等待确认信息;接收端成功接收新数据后返回确认信息。若在定时器超时前数据未能被确认,TCP就认为报文段中的数据已丢失或损坏,需要对报文段中的数据重新组织和重传。(重传超时时间: RTO)

在这里插入图片描述
四次挥手
在这里插入图片描述
1.客户端发送断开TCP连接请求的报文,其中报文中包含seq序列号,是由发送端随机生成的,并且还将报文中的FIN字段置为1,表示需要断开TCP连接。(FIN=1,seq=x,x由客户端随机生成)

2.服务端会回复客户端发送的TCP断开请求报文,其包含seq序列号,是由回复端随机生成的,而且会产生ACK字段,ACK字段数值是在客户端发过来的seq序列号基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP断开请求已经得到验证。(FIN=1,ACK=x+1,seq=y,y由服务端随机生成)

3.服务端在回复完客户端的TCP断开请求后,不会马上进行TCP连接的断开,服务端会先确保断开前,所有传输到A的数据是否已经传输完毕,一旦确认传输数据完毕,就会将回复报文的FIN字段置1,并且产生随机seq序列号。(FIN=1,ACK=x+1,seq=z,z由服务端随机生成)

4.客户端收到服务端的TCP断开请求后,会回复服务端的断开请求,包含随机生成的seq 字段和ACK字段,ACK字段会在服务端的TCP断开请求的seq基础上加1,从而完 成服务端请求的验证回复。(FIN=1,ACK=z+1,seq=h,h为客户端随机生成)

至此TCP断开的4次挥手过程完毕。

分包和粘包

TCP分包
场景:发送方发送字符串”helloworld”,接收方却分别接收到了两个数据包:字符串”hello”和”world”
发送端发送了数量较多的数据,接收端读取数据时候数据分批到达,造成一次发送多次读取;

造成分包的原因:
TCP是以段(Segment)为单位发送数据的,建立TCP链接后,有一个最大消息长度(MSS).如果应用层数据包超过MSS,就会把应用层数据包拆分,分成两个段来发送. 这个时候接收端的应用层就要拼接这两个TCP包,才能正确处理数据。相关的,路由器有一个MTU( 最大传输单元)一般是1500字节,除去IP头部20字节,留给TCP的就只有MTU-20字节。所以一般TCP的MSS为MTU-20=1460字节 当应用层数据超过1460字节时,TCP会分多个数据包来发送。

TCP 粘包
场景:发送方发送字符串”helloworld”,接收方却接收到了两个字符串”hello”和”world”
发送端发送了几次数据,接收端一次性读取了所有数据,造成多次发送一次读取;通常是网络流量优化,把多个小的数据段集满达到一定的数据量,从而减少网络链路中的传输次数

造成TCP粘包的原因:
TCP为了提高网络的利用率,会使用一个叫做Nagle的算法.该算法是指,发送端即使有要发送的数据,如果很少的话,会延迟发送.如果应用层给TCP传送数据很快的话,就会把两个应用层数据包“粘”在一起,TCP最后只发一个TCP数据包给接收端.

分包和粘包解决方案:
发送数据前,给数据附加两字节的长度:
4字节 N个字节
FBEB 数据长度N 数据内容

  1. 包标识: 包头部的特殊标识,用来标识包的开始
  2. 数据长度:数据包的大小,固定长度,2、4 或者8字节。
  3. 数据内容:数据内容,长度为数据头定义的长度大小。

实际操作如下:
a)发送端:先发送包表示和长度,再发送数据内容。
b)接收端:先解析本次数据包的大小N,再读取N个字节,这N个字节就是一个完整的数据内容。

具体流程如下:
在这里插入图片描述
UDP通信

  1. TCP与UDP
    当使用网络套接字通信时,
    套接字的“域”都取AF_INET;
    套接字的type:
    SOCK_STREAM 此时,默认使用TCP协议进行通信。
    SOCK_DGRAM 此时,默认使用UDP协议进行通信。

    TCP通信,是一个有序的、可靠的、面向连接的
    UDP通信,是不保证有序到达的数据报服务。(在局域网内,使用UDP已很可靠)

  2. 使用UDP通信
    与TCP通信使用上的区别:
    1)创建套接字时的type(参数2)不同。
    TCP通信,使用SOCK_STREAM
    UDP通信,使用SOCK_DGRAM

    2)发送数据和接收数据时,使用的接口不同
    TCP通信,发送数据,使用write(或send)
    接收数据,使用read(或recv)
    UDP特性,发送数据,使用sendto
    接收数据,服务器端使用recvfrom
    客户端使用recv

3)不需要使用listen

4)不需要先建立连接(TCP客户端和服务器端分别使用connect和receive建立连接)

步骤总结:
基于UDP的网络套接字通信
服务器端
(1) 创建一个网络套接字
(2) 设置服务器地址
(3) 绑定该套接字,使得该套接字和对应的端口关联起来
(4) 循环处理客户端请求使用recvfrom等待接收客户端发送的数据使用sendto发送数据至客户端

客户端
(1) 创建一个套接字
(2) 设置服务器地址
(3) 使用sendto向服务器端(接收端)发送数据
(4) 使用recv接受数据

  1. sendto与recvfrom、recv
    1. sendto
      功能:UDP服务器或客户端用于发送数据
      原型:int sendto (int sockfd, // 套接字
      void *buff, // 发送缓存区
      size_t len, // 发送缓冲区的长度
      init flags, // 标志,一般取0
      struct sockaddr *to, // 目的主机地址
      socklen_t tolen // 目的主机地址长度
      );
      返回值:成功,返回实际发送数据的字节数
      失败,返回-1
  1. recvfrom
    功能:UDP服务器用于接收数据
    原型: 与sendto类似。
    int recvfrom (int sockfd, // 套接字
    void *buff, // 接收缓存区
    size_t len, // 接受缓冲区的长度
    init flags, // 标志,一般取0
    struct sockaddr *to, // 源主机地址
    socklen_t *tolen // 源主机地址长度
    );
    注意:参数6必须要初始化为对应地址的长度!

  2. recv
    功能:UDP客户端用于接收数据
    原型: ssize_t recv (int sockfd, void *buf, size_t len, int flags);
    注意: 该调用的参数不需要指定地址。

因为当使用udp时,对应的套接字被自动绑定在一个短暂的动态的端口上。

实例1: 服务器接收、客户端发送
client1.c

#include <sys/un.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define BUFF_SIZE 1024

int main(void)
{
	int sockfd;
	struct sockaddr_in server_addr;
	int ret;
	int c;
	char buff[BUFF_SIZE];

	// 创建一个套接字
	sockfd = socket(AF_INET, SOCK_DGRAM, 0);

	// 设置服务器地址
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = inet_addr("10.10.0.9");
	server_addr.sin_port = htons(9000);

       // 向服务器发送数据
       strcpy(buff, "hello world");
       ret = sendto(sockfd, buff, strlen(buff) + 1, 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if (ret == -1) {
		perror("sendto");
		exit(errno);
	}

	printf("ret = %d\n", ret);
	
	
	return 0;	
}

server1.c

#include <sys/un.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define BUFF_SIZE 1024

int main(void)
{
	int server_sockfd;
	int client_sockfd;
	char ch;
	int ret;
	int recv_len;
	char buff[BUFF_SIZE];

	 //用于UNIX系统内部通信的地址, struct sockaddr_un
	struct sockaddr_in server_addr;
	struct sockaddr_in client_addr;
	int client_addr_len =sizeof(struct sockaddr_in);
	
	server_sockfd = socket(AF_INET, SOCK_DGRAM, 0);

	// 设置服务器地址
	server_addr.sin_family = AF_INET;  //地址的域,相当于地址的类型, AF_UNIX表示地址位于UNIX系统内部
	server_addr.sin_addr.s_addr = INADDR_ANY;  //inet_addr("10.10.0.9");
	server_addr.sin_port = htons(9000);

	// 绑定该套接字,使得该套接字和对应的系统套接字文件关联起来。
	ret = bind(server_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if (ret == -1) {
		perror("bind");
		exit(1);
	}

	// 创建套接字队列, 保存进入该服务器的客户端请求。
	//ret = listen(server_sockfd, 5);

	// 循环处理客户端请求
	while (1) {

		printf("server waiting\n");
		
		// 等待并接收客户端请求
		//client_sockfd = accept(server_sockfd,  (struct sockaddr*)&client_addr, &client_addr_len);
              recv_len = recvfrom(server_sockfd, buff, sizeof(buff) , 0, 
                                           (struct sockaddr*)&client_addr, &client_addr_len);
		if (recv_len < 0) {
			perror("recvfrom");
			exit(errno);
		}

		printf("received: %s\n", buff);	
	}

	close(server_sockfd);

	return 0;	
}

实例2:服务器收发、客户方发送、接收。
client2.c

#include <sys/un.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define BUFF_SIZE 1024

int main(void)
{
	int sockfd;
	struct sockaddr_in server_addr;
	int ret;
	int c;
	char buff[BUFF_SIZE];
	socklen_t addr_len;

	// 创建一个套接字
	sockfd = socket(AF_INET, SOCK_DGRAM, 0);

	// 设置服务器地址
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = inet_addr("10.10.0.99");
	server_addr.sin_port = htons(9000);
	
       // 向服务器发送数据
       strcpy(buff, "hello world");
       ret = sendto(sockfd, buff, strlen(buff) + 1, 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if (ret == -1) {
		perror("sendto");
		exit(errno);
	}

	printf("send %d bytes\n", ret);

	ret = recv(sockfd, buff, sizeof(buff), 0);
	if (ret == -1) {
		perror("recvfrom");
		exit(errno);
	}

	printf("received %d bytes\n", ret);
	printf("received: %s\n", buff);	
	
	return 0;	
}

server2.c

#include <sys/un.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define BUFF_SIZE 1024

static void str2up(char *str)
{
	while(*str) {
		if (*str >= 'a'  && *str <= 'z') {
			*str = *str - 'a' + 'A';
		}

		str++;
	}
}

int main(void)
{
	int server_sockfd;
	int client_sockfd;
	char ch;
	int ret;
	int recv_len;
	int send_len;
	char buff[BUFF_SIZE];

	 //用于UNIX系统内部通信的地址, struct sockaddr_un
	struct sockaddr_in server_addr;
	struct sockaddr_in client_addr;
	int client_addr_len = sizeof(struct sockaddr_in);
	
	server_sockfd = socket(AF_INET, SOCK_DGRAM, 0);

	// 设置服务器地址
	server_addr.sin_family = AF_INET;  //地址的域,相当于地址的类型, AF_UNIX表示地址位于UNIX系统内部
	server_addr.sin_addr.s_addr = INADDR_ANY;  //inet_addr("10.10.0.9");
	server_addr.sin_port = htons(9000);

	// 绑定该套接字,使得该套接字和对应的系统套接字文件关联起来。
	ret = bind(server_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if (ret == -1) {
		perror("bind");
		exit(1);
	}

	// 创建套接字队列, 保存进入该服务器的客户端请求。
	//ret = listen(server_sockfd, 5);

	// 循环处理客户端请求
	while (1) {

		printf("server waiting\n");
		
		// 等待并接收客户端请求
		//client_sockfd = accept(server_sockfd,  (struct sockaddr*)&client_addr, &client_addr_len);
              recv_len = recvfrom(server_sockfd, buff, sizeof(buff) , 0, 
                                           (struct sockaddr*)&client_addr, &client_addr_len);
		if (recv_len < 0) {
			perror("recvfrom");
			exit(errno);
		}

		printf("received: %s\n", buff);	

		str2up(buff);
		send_len = sendto(server_sockfd, buff, strlen(buff)+1, 0,
			                    (struct sockaddr*)&client_addr, client_addr_len);
		if (send_len == -1) {
			perror("sendto");
			exit(errno);
		}

		//printf("send_len=%d\n", send_len);                  
	}

	close(server_sockfd);

	return 0;	
}

同步IO和异步IO
场景1: 小明去打开水,而开水塔此时没有水,小明在现场一直等待开水到来,或者不断的轮询查看是否有开水,直到有开水取到水为止,这是同步IO的一种案例!

同步IO的特点:
同步IO指的是用户进程触发I/O操作并等待或者轮询的去查看I/O操作是否就绪。
同步IO的执行者是IO操作的发起者。
同步IO需要发起者进行内核态到用户态的数据拷贝过程,所以这里必须阻塞

场景2: 小明去打开水,而开水塔此时没有水,开水塔的阿姨叫小明把水壶放到现场,来水后会帮他打好水,并打电话叫他来取,这是异步IO的一种案例!

异步IO的特点:
异步IO是指用户进程触发I/O操作以后就立即返回,继续开始做自己的事情,而当I/O操作已经完成的时候会得到I/O完成的通知。
异步IO的执行者是内核线程,内核线程将数据从内核态拷贝到用户态,所以这里没有阻塞

五种网络IO模式
 对于一次IO访问(以read为例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,会经历两个阶段:
  1、等待数据准备
2、将数据从内核拷贝到进程中

linux系统产生了下面五种网络模式的方案:
  1、阻塞IO(blocking IO)
  2、非阻塞IO(nonblocking IO)
  3、IO多路复用(IO multiplexing)
  4、信号驱动IO(signal driven IO)不常用
5、异步IO (asynchronous IO)

阻塞IO
小明同学急用开水,打开水时发现开水龙头没水,他一直等待直到装满水然后离开。这一过程就可以看成是使用了阻塞IO模型,因为如果水龙头没有水,他也要等到有水并装满杯子才能离开去做别的事情。很显然,这种IO模型是同步的。
在linux 中,默认情况下所有的socket都是blocking IO, 一个典型的读操作流程:
在这里插入图片描述
非阻塞IO
小明同学又一次急用开水,打开水龙头后发现没有水,因为还有其它急事他马上离开了,过一会他又拿着杯子来看看……在中间离开的这些时间里,小明同学离开了装水现场(回到用户进程空间),可以做他自己的事情。这就是非阻塞IO模型。但是它只有是检查无数据的时候是非阻塞的,在数据到达的时候依然要等待复制数据到用户空间(等着水将水杯装满),因此它还是同步IO。

当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。

所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
典型的非阻塞IO模型一般如下:
在这里插入图片描述
设置非阻塞常用方式:
方式一: 创建socket 时指定
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);

方式二: 在使用前通过如下方式设定
fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);

IO多路复用
有一天,学校里面优化了热水的供应,增加了很多水龙头,这个时候小明同学再去装水,舍管阿姨告诉他这些水龙头都还没有水,你可以去忙别的了,等有水了告诉他。于是等啊等(select调用中),过了一会阿姨告诉他有水了。

这里有两种情况:
情况1: 阿姨只告诉来水了,但没有告诉小明是哪个水龙头来水了,要自己一个一个去尝试。(select/poll 场景)

情况2: 舍管阿姨会告诉小明同学哪几个水龙头有水了,小明同学不需要一个个打开看(epoll 场景)

在这里插入图片描述
当用户进程调用了select,那么整个进程就会被block,而同时,kernel会 “监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。所以,IO多路复用的特点是通过一种机制,一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入就绪状态,select()函数就可以返回。
这里需要使用两个system call(select 和 recvfrom),而blocking IO只调用了一个system call(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用mutil-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll 的优势并不是对于单个连接能处理得更好,而是在于能同时处理更多的连接。

SELECT
在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和异常等事件。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds : 最大的文件描述符加1。
readfds: 用于检查可读的。
writefds:用于检查可写性。
exceptfds:用于检查异常的数据。
timeout:一个指向timeval结构的指针,用于决定select等待I/o的最长时间。如果为空将一直等待。

timeval结构的定义:
struct timeval{
long tv_sec; // seconds
long tv_usec; // microseconds
};

返回值: >0 是已就绪的文件句柄的总数, =0 超时, <0 表示出错,错误: errno

#include <sys/select.h>
int FD_ZERO(fd_set *fdset); //一个 fd_set类型变量的所有位都设为 0
int FD_CLR(int fd, fd_set *fdset); //清除某个位时可以使用
int FD_SET(int fd, fd_set *fd_set); //设置变量的某个位置位
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位

经典案例:
服务器端 server.c

#include <sys/types.h> 
#include <sys/socket.h> 
#include <stdio.h> 
#include <netinet/in.h> 
#include <sys/time.h> 
#include <sys/ioctl.h> 
#include <unistd.h> 
#include <stdlib.h>

int main()
{
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    int result;
    fd_set readfds, testfds;
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket 
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(9000);
    server_len = sizeof(server_address);
    bind(server_sockfd, (struct sockaddr*)&server_address, server_len);
    listen(server_sockfd, 5); //监听队列最多容纳5个 
    FD_ZERO(&readfds);
    FD_SET(server_sockfd, &readfds);//将服务器端socket加入到集合中
    while (1)
    {
        char ch;
        int fd;
        int nread;
        testfds = readfds;//将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量 
        printf("server waiting\n");

        /*无限期阻塞,并测试文件描述符变动 */
        result = select(FD_SETSIZE, &testfds, (fd_set*)0, (fd_set*)0, (struct timeval*)0); //FD_SETSIZE:系统默认的最大文件描述符
        if (result < 1)
        {
            perror("server5");
            exit(1);
        }

        /*扫描所有的文件描述符*/
        for (fd = 0; fd < FD_SETSIZE; fd++)
        {
            /*找到相关文件描述符*/
            if (FD_ISSET(fd, &testfds))
            {
                /*判断是否为服务器套接字,是则表示为客户请求连接。*/
                if (fd == server_sockfd)
                {
                    client_len = sizeof(client_address);
                    client_sockfd = accept(server_sockfd,
                        (struct sockaddr*)&client_address, &client_len);
                    FD_SET(client_sockfd, &readfds);//将客户端socket加入到集合中
                    printf("adding client on fd %d\n", client_sockfd);
                }
                /*客户端socket中有数据请求时*/
                else
                {
                    ioctl(fd, FIONREAD, &nread);//取得数据量交给nread

                    /*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */
                    if (nread == 0)
                    {
                        close(fd);
                        FD_CLR(fd, &readfds); //去掉关闭的fd
                        printf("removing client on fd %d\n", fd);
                    }
                    /*处理客户数据请求*/
                    else
                    {
                        read(fd, &ch, 1);
                        sleep(5);
                        printf("serving client on fd %d\n", fd);
                        ch++;
                        write(fd, &ch, 1);
                    }
                }
            }
        }
    }

    return 0;
}

客户端

#include <sys/types.h> 
#include <sys/socket.h> 
#include <stdio.h> 
#include <netinet/in.h> 
#include <arpa/inet.h> 
#include <unistd.h> 
#include <stdlib.h>
#include <sys/time.h>

int main()
{
    int client_sockfd;
    int len;
    struct sockaddr_in address;//服务器端网络地址结构体 
    int result;
    char ch = 'A';
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket 
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(9000);
    len = sizeof(address);
    result = connect(client_sockfd, (struct sockaddr*)&address, len);
    if (result == -1)
    {
        perror("oops: client2");
        exit(1);
    }
    //第一次读写
    write(client_sockfd, &ch, 1);
    read(client_sockfd, &ch, 1);
    printf("the first time: char from server = %c\n", ch);
    sleep(5);

    //第二次读写
    write(client_sockfd, &ch, 1);
    read(client_sockfd, &ch, 1);
    printf("the second time: char from server = %c\n", ch);

    close(client_sockfd);

    return 0;
}

POLL
和select 一样,如果没有事件发生,则进入休眠状态,如果在规定时间内有事件发生,则返回成功,规定时间过后仍然没有事件发生则返回失败。可见,等待期间将进程休眠,利用事件驱动来唤醒进程,将更能提高CPU的效率。
poll 和select 区别: select 有文件句柄上线设置,值为FD_SETSIZE,而poll 理论上没有限制!
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

输入参数:
fds://可以传递多个结构体,也就是说可以监测多个驱动设备所产生的事件,只要有一个产生了请求事件,就能立即返回
struct pollfd {
int fd; /文件描述符 open打开的那个/
short events; /请求的事件类型,监视驱动文件的事件掩码/ POLLIN | POLLOUT
short revents; /驱动文件实际返回的事件/
};
nfds: //监测驱动文件的个数
timeout://超时时间,单位是ms

事件类型events 可以为下列值:
POLLIN 有数据可读
POLLRDNORM 有普通数据可读,等效与POLLIN
POLLPRI 有紧迫数据可读
POLLOUT 写数据不会导致阻塞
POLLER 指定的文件描述符发生错误
POLLHUP 指定的文件描述符挂起事件
POLLNVAL 无效的请求,打不开指定的文件描述符
返回值:
有事件发生 返回revents域不为0的文件描述符个数
超时:return 0
失败:return -1 错误:errno

服务器端 server_poll.c

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>

#define MAX_FD  8192
struct pollfd  fds[MAX_FD];
int cur_max_fd = 0;


int main()
{
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    int result;
    //fd_set readfds, testfds;
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(9000);
    server_len = sizeof(server_address);
    bind(server_sockfd, (struct sockaddr*)&server_address, server_len);
    listen(server_sockfd, 5); //监听队列最多容纳5个
    //FD_ZERO(&readfds);
    //FD_SET(server_sockfd, &readfds);//将服务器端socket加入到集合中
    fds[server_sockfd].fd = server_sockfd;
    fds[server_sockfd].events = POLLIN;
    fds[server_sockfd].revents = 0;
    if(cur_max_fd <= server_sockfd)
    {
        cur_max_fd = server_sockfd + 1;
    }


    while (1)
    {
        char ch;
        int i, fd;
        int nread;
        //testfds = readfds;//将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量
        printf("server waiting\n");

        /*无限期阻塞,并测试文件描述符变动 */
        result = poll(fds, cur_max_fd, 1000);
        //result = select(FD_SETSIZE, &testfds, (fd_set*)0, (fd_set*)0, (struct timeval*)0); //FD_SETSIZE:系统默认的最大文件描述符
        if (result < 0)
        {
            perror("server5");
            exit(1);

        }
        /*扫描所有的文件描述符*/
        for (i = 0; i < cur_max_fd; i++)
        {

            /*找到相关文件描述符*/
            if (fds[i].revents)
            {
                fd = fds[i].fd;
                /*判断是否为服务器套接字,是则表示为客户请求连接。*/
                if (fd == server_sockfd)
                {
                    client_len = sizeof(client_address);
                    client_sockfd = accept(server_sockfd,
                        (struct sockaddr*)&client_address, &client_len);
                    fds[client_sockfd].fd = client_sockfd;//将客户端socket加入到集合中
                    fds[client_sockfd].events = POLLIN;
                    fds[client_sockfd].revents = 0;


                    if(cur_max_fd <= client_sockfd)
                    {
                        cur_max_fd = client_sockfd + 1;
                    }

                    printf("adding client on fd %d\n", client_sockfd);
                    //fds[server_sockfd].events = POLLIN;
                }
                /*客户端socket中有数据请求时*/
                else
                {
                    //ioctl(fd, FIONREAD, &nread);//取得数据量交给nread
                    nread = read(fd, &ch, 1);
                    /*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */
                    if (nread == 0)
                    {
                        close(fd);
                        memset(&fds[i], 0, sizeof(struct pollfd)); //去掉关闭的fd
                        printf("removing client on fd %d\n", fd);
                    }
                    /*处理客户数据请求*/
                    else
                    {
                        //read(fds[fd].fd, &ch, 1);
                        sleep(5);
                        printf("serving client on fd %d, read: %c\n", fd, ch);
                        ch++;
                        write(fd, &ch, 1);
                        //fds[fd].events = POLLIN;
                    }
                }
            }
        }
    }

    return 0;
}

EPOLL
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
epoll_web_server.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<assert.h>
#include<fcntl.h>
#include<unistd.h>

//    int fd;
typedef struct _ConnectStat  ConnectStat;

typedef void(*response_handler) (ConnectStat * stat);

struct _ConnectStat {
	int fd;
	char name[64];
	char  age[64];
	struct epoll_event _ev;
	int  status;//0 -未登录   1 - 已登陆
	response_handler handler;//不同页面的处理函数
};

//http协议相关代码
ConnectStat * stat_init(int fd);
void connect_handle(int new_fd);
void do_http_respone(ConnectStat * stat);
void do_http_request(ConnectStat * stat);
void welcome_response_handler(ConnectStat * stat);
void commit_respone_handler(ConnectStat * stat);


const char *main_header = "HTTP/1.0 200 OK\r\nServer: Martin Server\r\nContent-Type: text/html\r\nConnection: Close\r\n";

static int epfd = 0;

void usage(const char* argv)
{
	printf("%s:[ip][port]\n", argv);
}

void set_nonblock(int fd)
{
	int fl = fcntl(fd, F_GETFL);
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int startup(char* _ip, int _port)  //创建一个套接字,绑定,检测服务器
{
	//sock
	//1.创建套接字
	int sock = socket(AF_INET, SOCK_STREAM, 0);
	if (sock < 0)
	{
		perror("sock");
		exit(2);
	}

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

	//2.填充本地 sockaddr_in 结构体(设置本地的IP地址和端口)
	struct sockaddr_in local;
	local.sin_port = htons(_port);
	local.sin_family = AF_INET;
	local.sin_addr.s_addr = inet_addr(_ip);

	//3.bind()绑定
	if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
	{
		perror("bind");
		exit(3);
	}
	//4.listen()监听 检测服务器
	if (listen(sock, 5) < 0)
	{
		perror("listen");
		exit(4);
	}
	//sleep(1000);
	return sock;    //这样的套接字返回
}

int main(int argc, char *argv[])
{
	if (argc != 3)     //检测参数个数是否正确
	{
		usage(argv[0]);
		exit(1);
	}

	int listen_sock = startup(argv[1], atoi(argv[2]));      //创建一个绑定了本地 ip 和端口号的套接字描述符


	//1.创建epoll    
	epfd = epoll_create(256);    //可处理的最大句柄数256个
	if (epfd < 0)
	{
		perror("epoll_create");
		exit(5);
	}

	struct epoll_event _ev;       //epoll结构填充 
	ConnectStat * stat = stat_init(listen_sock);
	_ev.events = EPOLLIN;         //初始关心事件为读
	_ev.data.ptr = stat;
	//_ev.data.fd = listen_sock;    //  

	//2.托管
	epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &_ev);  //将listen sock添加到epfd中,关心读事件

	struct epoll_event revs[64];

	int timeout = -1;
	int num = 0;
	int done = 0;

	while (!done)
	{
		//epoll_wait()相当于在检测事件
		switch ((num = epoll_wait(epfd, revs, 64, timeout)))  //返回需要处理的事件数目  64表示 事件有多大
		{
		case 0:                  //返回0 ,表示监听超时
			printf("timeout\n");
			break;
		case -1:                 //出错
			perror("epoll_wait");
			break;
		default:                 //大于零 即就是返回了需要处理事件的数目
		{
			struct sockaddr_in peer;
			socklen_t len = sizeof(peer);

			int i;
			for (i = 0; i < num; i++)
			{
				ConnectStat * stat = (ConnectStat *)revs[i].data.ptr;

				int rsock = stat->fd; //准确获取哪个事件的描述符
				if (rsock == listen_sock && (revs[i].events) && EPOLLIN) //如果是初始的 就接受,建立链接
				{
					int new_fd = accept(listen_sock, (struct sockaddr*)&peer, &len);

					if (new_fd > 0)
					{
						printf("get a new client:%s:%d\n", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
						//sleep(1000);
						connect_handle(new_fd);
					}
				}
				else // 接下来对num - 1 个事件处理
				{
					if (revs[i].events & EPOLLIN)
					{
						do_http_request((ConnectStat *)revs[i].data.ptr);
					}
					else if (revs[i].events & EPOLLOUT)
					{
						do_http_respone((ConnectStat *)revs[i].data.ptr);
					}
					else
					{
					}
				}
			}
		}
		break;
		}//end switch
	}//end while
	return 0;
}


ConnectStat * stat_init(int fd) {
	ConnectStat * temp = NULL;
	temp = (ConnectStat *)malloc(sizeof(ConnectStat));

	if (!temp) {
		fprintf(stderr, "malloc failed. reason: %m\n");
		return NULL;
	}

	memset(temp, '\0', sizeof(ConnectStat));
	temp->fd = fd;
	temp->status = 0;
	//temp->handler = welcome_response_handler;

}

//初始化连接,然后等待浏览器发送请求
void connect_handle(int new_fd) {
	ConnectStat *stat = stat_init(new_fd);
	set_nonblock(new_fd);

	stat->_ev.events = EPOLLIN;
	stat->_ev.data.ptr = stat;

	epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, &stat->_ev);    //二次托管

}

void do_http_respone(ConnectStat * stat) {
	stat->handler(stat);
}

void do_http_request(ConnectStat * stat) {

	//读取和解析http 请求
	char buf[4096];
	char * pos = NULL;
	//while  header \r\n\r\ndata
	ssize_t _s = read(stat->fd, buf, sizeof(buf) - 1);
	if (_s > 0)
	{
		buf[_s] = '\0';
		printf("receive from client:%s\n", buf);

		pos = buf;

		//Demo 仅仅演示效果,不做详细的协议解析
		if (!strncasecmp(pos, "GET", 3)) {
			stat->handler = welcome_response_handler;
		}
		else if (!strncasecmp(pos, "Post", 4)) {
			//获取 uri
			printf("---Post----\n");
			pos += strlen("Post");
			while (*pos == ' ' || *pos == '/') ++pos;

			if (!strncasecmp(pos, "commit", 6)) {//获取名字和年龄
				int len = 0;

				printf("post commit --------\n");
				pos = strstr(buf, "\r\n\r\n");
				char *end = NULL;
				if (end = strstr(pos, "name=")) {
					pos = end + strlen("name=");
					end = pos;
					while (('a' <= *end && *end <= 'z') || ('A' <= *end && *end <= 'Z') || ('0' <= *end && *end <= '9'))	end++;
					len = end - pos;
					if (len > 0) {
						memcpy(stat->name, pos, end - pos);
						stat->name[len] = '\0';
					}
				}

				if (end = strstr(pos, "age=")) {
					pos = end + strlen("age=");
					end = pos;
					while ('0' <= *end && *end <= '9')	end++;
					len = end - pos;
					if (len > 0) {
						memcpy(stat->age, pos, end - pos);
						stat->age[len] = '\0';
					}
				}
				stat->handler = commit_respone_handler;

			}
			else {
				stat->handler = welcome_response_handler;
			}

		}
		else {
			stat->handler = welcome_response_handler;
		}

		//生成处理结果 html ,write

		stat->_ev.events = EPOLLOUT;
		//stat->_ev.data.ptr = stat;
		epoll_ctl(epfd, EPOLL_CTL_MOD, stat->fd, &stat->_ev);    //二次托管
	}
	else if (_s == 0)  //client:close
	{
		printf("client: %d close\n", stat->fd);
		epoll_ctl(epfd, EPOLL_CTL_DEL, stat->fd, NULL);
		close(stat->fd);
		free(stat);
	}
	else
	{
		perror("read");
	}

}


void welcome_response_handler(ConnectStat * stat) {
	const char * welcome_content = "\
<html lang=\"zh-CN\">\n\
<head>\n\
<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\n\
<title>This is a test</title>\n\
</head>\n\
<body>\n\
<div align=center height=\"500px\" >\n\
<br/><br/><br/>\n\
<h2>大家好,欢迎来到奇牛学院VIP 课!</h2><br/><br/>\n\
<form action=\"commit\" method=\"post\">\n\
尊姓大名: <input type=\"text\" name=\"name\" />\n\
<br/>芳龄几何: <input type=\"password\" name=\"age\" />\n\
<br/><br/><br/><input type=\"submit\" value=\"提交\" />\n\
<input type=\"reset\" value=\"重置\" />\n\
</form>\n\
</div>\n\
</body>\n\
</html>";

	char sendbuffer[4096];
	char content_len[64];

	strcpy(sendbuffer, main_header);
	snprintf(content_len, 64, "Content-Length: %d\r\n\r\n", (int)strlen(welcome_content));
	strcat(sendbuffer, content_len);
	strcat(sendbuffer, welcome_content);
	printf("send reply to client \n%s", sendbuffer);

	write(stat->fd, sendbuffer, strlen(sendbuffer));

	stat->_ev.events = EPOLLIN;
	//stat->_ev.data.ptr = stat;
	epoll_ctl(epfd, EPOLL_CTL_MOD, stat->fd, &stat->_ev);


}


void commit_respone_handler(ConnectStat * stat) {
	const char * commit_content = "\
<html lang=\"zh-CN\">\n\
<head>\n\
<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\n\
<title>This is a test</title>\n\
</head>\n\
<body>\n\
<div align=center height=\"500px\" >\n\
<br/><br/><br/>\n\
<h2>欢迎学霸同学&nbsp;%s &nbsp;,你的芳龄是&nbsp;%s!</h2><br/><br/>\n\
</div>\n\
</body>\n\
</html>\n";

	char sendbuffer[4096];
	char content[4096];
	char content_len[64];
	int len = 0;

	len = snprintf(content, 4096, commit_content, stat->name, stat->age);
	strcpy(sendbuffer, main_header);
	snprintf(content_len, 64, "Content-Length: %d\r\n\r\n", len);
	strcat(sendbuffer, content_len);
	strcat(sendbuffer, content);
	printf("send reply to client \n%s", sendbuffer);

	write(stat->fd, sendbuffer, strlen(sendbuffer));

	stat->_ev.events = EPOLLIN;
	//stat->_ev.data.ptr = stat;
	epoll_ctl(epfd, EPOLL_CTL_MOD, stat->fd, &stat->_ev);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值