使用Socket网络编程实现TCP/IP通信

1 基础

三次握手,四次挥手

1.1 三次握手

在这里插入图片描述

1.2 四次挥手

在这里插入图片描述

1.3 Socket实现TCP通信流程

传统的TCP/IP通信过程依赖于socket,位于应用层和传输层之间,使得应用程序可以进行通信。相当于港口城市的码头,使得城市之间可以进行货物流通。服务器和客户端各有不同的通信流程。在这里插入图片描述

Socket编程实现TCP通信的流程如下图:
我们的目标就是编程实现这个流程。

在这里插入图片描述

2 Socker基础

要想完成网络编程,需要了解基础:网络编程API

2.1 Socket地址API

socket含义是:IP和端口对 (ip,port),它唯一表示了TCP通信的一端。也称为socket地址
要学习socket地址,需要先学习主机字节序和网络字节序

2.1.1 字节序

为字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序

字节序分为两类:大端字节序和小端字节序

  • Big-Endian:是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
  • Little-Endian:就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

可以通过代码判断当前主机的字节序类型,联合体共用同一内存,可以看0x0102如何存储的。

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
	union {
		short s;
		char c[sizeof(short)];
	} un;
	un.s = 0x0102;
	if (un.c[0] == 1 && un.c[1] == 2){
		printf("Big-Endian\n");
	}
	if (un.c[0] == 2 && un.c[1] == 1){
		printf("Little-Endian\n");
	}
	exit(0);
}

2.1.2 主机字节序 & 网络字节序

  • 现代PC绝大多数都是小端的字节序,因此小端字节序称为主机字节序

  • 大端也称为网络字节序

  • 多台电脑进行通信时,如果双方是不同的字节序,会造成通信解析错误。解决办法:发送端总是采用大端字节序,接收端根据自身的字节序决定是否对接收的数据进行转换(小端转换,大端不转)

为了进行转换 bsd socket提供了转换的函数 有下面四个
在这里插入图片描述

  • htons 把unsigned short类型从主机序转换到网络序
  • htonl 把unsigned long类型从主机序转换到网络序
  • ntohs 把unsigned short类型从网络序转换到主机序
  • ntohl 把unsigned long类型从网络序转换到主机序

长整型通常转换IP,短整型转换端口

2.1.3 通用socket地址

表示socket地址的是结构体socketaddr

#include <bits/socket.h>
struct socketaddr
{
	sa_family_t sa_family;
	char sa_data[14];
}
  • sa_family是地址族变量,通常地址族和协议族对应

在这里插入图片描述

  • sa_data存放socket地址值,不同的协议对应的地址值具有不同的含义和地址
    在这里插入图片描述

2.1.4 专用socket地址

上述的通用地址不好用,故针对UNIX、IPv4、IPv6分别定义了不同的专用socket地址结构体,此时列出IPv4的结构体:socketaddr_in

struct socketaddr_in
{
	sa_family_t sin_family;
	u_int16_t sin_port;
	struct in_addr sin_addr;
}
struct in_addr
{
	u_int32_t s_addr;
}

在这里插入图片描述
所有专用的socket地址类型的变量在实际使用时都要转化为通用socket地址类型socketaddr(强制类型转换)
在这里插入图片描述

在这里插入图片描述

2.1.5 IP地址转换函数:

人们习惯字符串表示IP地址

  • 点分十进制表示IPv4
  • 点分十六进制表示IPv6

但是,编程中我们需要字符串转为整数二进制。日志中则需要把整数IP转为字符串。
因此,三个函数可以将点分十进制字符串表示的IPv4地址和用网络字节序整数表示的地址进行转换
在这里插入图片描述
如inet_addr输入点分十进制字符串返回网络字符串整数。失败则返回INADDR_NONE
在这里插入图片描述

2.2 socket基础API

包括

  • 创建socket:socket函数
  • socket选项 :getsocket/setsocket函数
  • 命名socket:bind函数
  • 监听socket:listen函数
  • 接受连接:accept函数
  • 发起连接:connect函数
  • 读写数据:send、recv函数
  • 关闭连接:close/shutdown函数
  • 获取地址信息:getsockname/getpeername函数
  • 检测带外标记:sockmark函数

返回值若为errno(Linux提供的),表示各种错误

2.2.1 创建socket:socket函数

Linux哲学:所有东西都是文件
socket是可读可写可控制可关闭的文件描述符,使用socket系统调用创建一个socket:
在这里插入图片描述

  • domain参数是用哪个底层协议族
  • type是指定服务类型,如SOCK-STREAM(流服务TCP) / SOCK_UGRAM(数据报UDP)(新内核中可以与 SOCK_NONBLOCK和SOCK_CLOEXEC标志与)
  • protocol是在前两个协议下,再选择一个具体协议,默认协议为0
  • 系统调用成功则返回一个socket文件描述符,失败则返回-1并设置errno

如图为系统调用TCP协议下IPv4的socket

在这里插入图片描述

2.2.2 socket选项 (读取和设定:getsocket/setsocket函数)

专门读取和设定socket文件描述符属性:
在这里插入图片描述

  • sockfd指定被操作的目标socket
  • level指定操作那个协议的选项,比如IPv4/IPv6/TCP等
  • option_name指定选项的名字
  • option_value和option_len分别是被操作选项的值和长度。
  • 成功时都是返回0,否则返回-1且设置errno

重要的几个选项:

  1. SO_REUSEADDR:端口处于WAIT_TIME仍然可以启动
  2. SO_RCVBUF:TCP接收缓冲区大小
  3. SO_SNDBUF:发送缓冲区大小
    在这里插入图片描述

2.2.3 命名socket:bind函数

创建socket指定了地址族,但没有指定具体socket地址。将socket指定具体的socket地址称为命名(绑定)。只有服务器端命名后,客户端才知道如何连接它。客户端一般不绑定,而是匿名方式自动分配socket地址
在这里插入图片描述
bind将my_addr所指向的socket地址分配给未命名的sockfd文件描述符,addrlen指出该socket地址长度。

  • bind成功返回0,失败返回-1并设置errno
  1. EACCES 被绑定的socket地址是受保护的地址,仅超级用户可以访问,如绑定知名服务端口0~1023
  2. EADDRINUSE 被绑定的地址正在使用中,如将socket绑定到一个处于time_wait状态的socket地址

2.2.4 监听socket:listen函数

被命名后,还不能马上连接客户端,还需要系统调用创建监听队列存放待处理的客户连接
在这里插入图片描述

  • sockfd指定被监听的socket
  • backlog提示内核监听队列的最大长度,典型值为5(超过长度则不接受新的客户连接,客户端收到ECONNREFUSED错误)
  • 成功则返回0,失败返回-1且设置errno

eg:10个客户端连接服务器,最大值设为5,则6个(5+1)处于完全连接(ESTABLISHED),5个处于半连接状态(SYN_RCVD)

2.2.5 发起连接:connect函数

服务器通过listen被动接受连接,客户端用connect主动与服务端建立连接:
在这里插入图片描述

  • sockfd是2.2.1socket函数系统调用后返回的socket
  • serv_addr服务器监听的socket地址,addrlen指定了地址长度
  • 成功则返回0,否则返回-1且设置errno,常见的
    1.ECONNREFUSED 目标端口不存在
    2.ETIMEDOUT 链接超时
  • 一旦成功建立,sockfd唯一标识了这个连接,客户端通过读写sockfd与服务区通信

2.2.6 接受连接:accept函数

函数从监听队列中接收一个进行连接:
在这里插入图片描述

  • sockfd是listen中的第一个监听参数socket
  • addr获取被连接的远端socket地址,长度是addrlen(如果是专用socket结构体,需要强制类型转换)
  • accept成功则返回新的socket,失败则返回-1,且设置errno

2.2.7 读写数据:send/recv函数

文件操作read和write也可以使用于socket,但是他也有专用的API:其中用于TCP流数据读写的系统调用是:在这里插入图片描述

  • recv读取sockfd上数据,buf和len分别制定缓冲区的位置和大小
  • recv成功则返回读取的长度,它可能小于我们期望的长度,故需要多次调用recv,才能完整读取。返回0这说明通信对方关闭了。返回-1则出错了,且设置errno
  • send往sockfd写数据,buf和len指定写数据的缓冲区位置和大小。send成功返回时及写入的数据长度,失败则返回-1且设置errno。
  • flags是为数据收发提供了额外的控制在这里插入图片描述

2.2.8 关闭连接:close/shutdown函数

关闭这个连接实际就是关闭连接对应的socket:可以使用普通文件描述符:
在这里插入图片描述

  • fd是待关闭的socket,不是立即关闭该链接,而是fd的引用计数减1.
    只有当fd引用计数为0,真正关闭。多进程程序中,fork调用会使父进程中计数+1,只有父子进程均close关闭socket,才算退出。
  • 如果想立即退出,则使用shutdown系统调用(相对于close,它是专门的网络编程设计)
    在这里插入图片描述
  • sockfd是待关闭的socket,howto则决定了shutdown的行为:在这里插入图片描述
  • shutdown成功返回0,失败返回-1且设置errno

2.2.9 获取地址信息:getsockname/getpeername函数

在这里插入图片描述

2.2.10 检测带外标记:sockmark函数

在这里插入图片描述

2.2.11 其他

通用数据读写函数
在这里插入图片描述
UDP读写:
在这里插入图片描述
网络信息API:主机名访问机器,服务名称访问端口号
在这里插入图片描述
等等。

3 TCP/IP通信

3.1 IP协议

IP协议是TCP/IP协议族的核心协议,也是socket基础。

  • IP头部信息:指定源端、目的端IP地址,IP切片等
  • IP数据报路由和转发:决定数据报是否应该转发和如何转发。

IP协议为上层协议提供无状态、无连接、不可靠的服务。

3.1.1 IP头部

主要是IPv4
在这里插入图片描述

  • 长度一般20字节,除非有可变长的选项
  • 4位版本号:指定IP协议的版本(IPv4为4)
  • 4位头部长度:该IP头部有多少个4字节(32位)(4位最大15,故最大15*4=60字节)
  • 8位服务类型:如最小延时(ssh、telnet)、最大吞吐量(ftp)
  • 16位总长度是IP数据报的长度,受MTU限制,长度超过MTU则分片传输(实际传输的IP数据报长度都没有超过最大值)接下三个字段表示分片
  • 16位标识:唯一标识主机发送的每一个数据报,系统随机生成;每发送一个数据报,值加1。如果分片,则每个数据报标识相同。
  • 3位标识,第一字段保留、第二字段进制分片,第三标识更多分片
  • 13位分片偏移分片相对于原始数据报的偏移。
  • 8位生存时间:数据报到达目的地之前允许的最大路由器跳数。TTL减为0,发送ICMP差错报文。
  • 8位协议区分上层协议,ICMP=1,TCP=6,UDP=17等。
  • 16位头部校验和,检验IP数据报头部在传输过程是否损坏。
  • 32位的源端IP和目的端IP表示数据包的发送端和接收端,整个传输保持不变。
  • 最后是选项字段,最多包含40字节,如记录路由、时间戳、松散路由源选择等

3.1.2 tcpdump观察IP头部

执行telnet登陆本机,抓取客户机与服务器之间交换的数据包:

在这里插入图片描述
在这里插入图片描述

  • 源端IP和目的端IP都是127.0.0.1
  • 服务器端口23,客户端使用临时端口58422与服务器进行通信
  • 抓包开启了x选型,告诉tcpdump命令,需要把协议头和包内容都原原本本的显示出来(tcpdump会以16进制和ASCII的形式显示),这在进行协议分析时是绝对的利器。,此数据包60字节。
  • 前20字节是IP头部,后40字节是TCP头部

可参考如下分析: 在这里插入图片描述

在这里插入图片描述

3.1.3 IP路由

决定数据报是否应该转发和如何转发。
路由工作流程、路由机制、IP转发、ICMP重定向等
路由相关命令:route、netstat等

3.2 TCP协议

和IP协议相比,更靠近应用层。

  • TCP头部,指定源端、目的端端口号,管理TCP,控制数据流等
  • TCP状态转移,连接的任意一段都是状态机。连接到断开,经历不同的状态变迁。
  • TCP数据流,交互数据流和成块数据流、紧急数据流
  • 数据流的控制:超时重传和拥塞控制

3.2.1 TCP头部

在这里插入图片描述

  • 16位端口号:告知该报文来自哪个源端口,传给哪个上层协议或目的端口。通信时,服务端一般采用知名的服务端口(/etc/services),客户端使用系统自动选择的临时端口号。
  • 32位序号:一次TCP通信(连接到断开)过程中某个传输方向上的字节流的字节编号。A发给B的报文段中序号值将被系统设置ISN加上该报文段的偏移,如传的1025~2048,则传输ISN+1025。
  • 32位确认号:用作对另一方发来报文段的响应。A发送的不仅携带自己的序号,也携带对B发送的数据的确认号。返回值=发送+1
  • 4位头部长度:四位最大60,头部最长60字节
  • 6位标志位
    1)URG :紧急指针是否有效
    2)ACK:表示确认号是否有效
    3)PSH:表示李继聪TCP接收缓冲区读走数据
    4)RST:要求对方重新建立连接
    5)SYN:请求建立一个连接,携带SYN标志的TCP报文段为同步报文。
    6)FIN:结束连接,携带FIN为结束报文段。
  • 16位窗口大小:TCP流量控制的手段
  • 16位校验和,检验TCP报文段是否损坏,可靠传输的保证
  • 16位紧急指针:正的偏移量,它和序号字段的和表示最后一个紧急数据的下一字节序号
  • TCP头部选项,最多包含40字节(总共60字节,前面固定长度20字节,1字节8位)

3.2.2 tcpdump观察tcp头部

执行telnet登陆本机,抓取客户机与服务器之间交换的数据包:

在这里插入图片描述
在这里插入图片描述
-输出Flags[S],表示含SYN标志,它是同步报文段。如果也含其他标志,则会显示其他标志的首字母

  • seq是序号值,因为这是第一个报文,所以它是ISN随机值,也没有确认值
  • win是接收通告窗口大小
  • options是TCP选项

参考分析如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.3 三次握手与四次挥手的socket分析

3.3.1 三次握手

在这里插入图片描述
通过tcpdump抓取通信过程:
在这里插入图片描述
在这里插入图片描述

3.3.2 四次挥手

在这里插入图片描述
通过tcpdump抓取通信过程:
在这里插入图片描述
在这里插入图片描述

3.1 服务端

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>//struct
#include <unistd.h>//close head
#include <string.h>//bzero
#include <stdlib.h>
#define PORT 8111
#define MESSAGE_SIZE 1024
int main(int argc, char* argv[])
{
	int socket_fd;
	int accept_fd;
	int backlog = 10;
	int ret = -1;
	int flag = 1;
	struct sockaddr_in local_addr, remote_addr;
	char in_buf[MESSAGE_SIZE] = {0,};
	
	//create socket
	socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (socket_fd == -1)
	{
		std::cout << "file to create socket!" << std::endl;
		exit(-1);
	}
	//set socket options
	ret = setsockopt(socket_fd, 
					SOL_SOCKET, 
					SO_REUSEADDR, 
					&flag, 
					sizeof(flag));
	if (ret == -1)
	{
		std::cout << "failed to set socket options!" << std::endl;
	}
	//set localaddr
	local_addr.sin_family = AF_INET;
	local_addr.sin_port = PORT;
	local_addr.sin_addr.s_addr = INADDR_ANY;//0 RENHE IP DOU LISTEN
	bzero(&(local_addr.sin_zero),8);
	//bind socket
	ret = bind(socket_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
	if (ret == -1)
	{
		std::cout << "failed to bind addr!" << std::endl;
		exit(-1);
	}
	//listen
	ret = listen(socket_fd, backlog);
	if (ret == -1)
	{
		std::cout << "failed to listen socket!" << std::endl;
		exit(-1);
	}
	for(;;)
	{
		socklen_t addr_len = sizeof(struct sockaddr);
		accept_fd  = accept(socket_fd, 
							(struct sockaddr *) &remote_addr, 
							&addr_len);
		for (;;)
		{
			ret = recv(accept_fd, (void *)in_buf, MESSAGE_SIZE, 0);
			if (ret == 0)
				break;
			std::cout << "receive: " << in_buf << std::endl;
			send(accept_fd, (void*)in_buf, MESSAGE_SIZE,0);
		}
		close(accept_fd);
	}
	close(socket_fd);
	return 0;
}

3.2 客户端

#include <iostream>
#include <sys/socket.h>//1connect
#include <sys/types.h>//2connect
#include <netinet/in.h> //struct
#include <string.h>//memset
#include <stdio.h>//gets
#include <unistd.h>//close
#include <arpa/inet.h>//inet
#include <stdlib.h>
#define PORT 8111
#define MESSAGE_LEN 1024
using namespace std;
int main(int argc, char* argv[])
{
	int socket_fd;
	int ret = -1;
	char sendbuf[MESSAGE_LEN] = {0,};
	char recvbuf[MESSAGE_LEN] = {0,};
	struct sockaddr_in serverAddr;
	socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (socket_fd < 0)
	{
		cout << "failed to create socket" << endl;
		exit(-1);
	}
	
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = PORT;
	serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	ret = connect(socket_fd, 
				(struct sockaddr *)&serverAddr, 
				sizeof(struct sockaddr));
	if (ret < 0)
	{
		cout << "failed to connect!" << endl;
		exit(-1);
	}
	while(1)
	{
		memset(sendbuf, 0, MESSAGE_LEN);
		//gets(sendbuf); 
        scanf("%s", &sendbuf[0]);
		ret = send(socket_fd, sendbuf, strlen(sendbuf), 0);
		if (ret <= 0)
		{
			cout << "failed to send data!" << endl;
			break;		
		}
		//guanbi 1 kill -9 1111      2 duibi
		if (strcmp(sendbuf, "quit") == 0)
		{
			break;
		}
		ret = recv(socket_fd, recvbuf, MESSAGE_LEN, 0);
		recvbuf[ret] = '\0';
		cout << "recv:" << recvbuf << endl;
	}
	close(socket_fd);
	return 0;
}

3.3 演示

在这里插入图片描述

4 实现并发服务器

在3中所演示的C/S通信架构,只有一对server和client时适用,当多个client发出连接请求则会失败;只有client结束,其余client才能抢占资源。

4.1 多进程解决:fork方式

解决方案:

  • 每收到一个连接就创建子进程
  • 父进程负责接受连接
  • 通过fork创建子进程

存在问题:

  • 资源被长期占用:只要长连接没有断开,则子进程一直被占用。(上万个被占用怎么办)
  • 创建子进程花费时间长(大量连接)
  • 父进程必须关闭已连接描述符,以防止内存泄漏,知道父子所有进程连接描述符关闭,连接才会终止。

4.1.1 服务端代码

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>//struct
#include <unistd.h>//close head
#include <string.h>//bzero
#define PORT 8111
#define MESSAGE_SIZE 1024
int main(int argc, char* argv[])
{
	int socket_fd;
	int accept_fd;
	int backlog = 10;
	int ret = -1;
	int flag = 1;
	pid_t pid;
	struct sockaddr_in local_addr, remote_addr;
	char in_buf[MESSAGE_SIZE] = {0,};
	
	//create socket
	socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (socket_fd == -1)
	{
		std::cout << "file to create socket!" << std::endl;
		exit(-1);
	}
	//set socket options
	ret = setsockopt(socket_fd, 
					SOL_SOCKET, 
					SO_REUSEADDR, 
					&flag, 
					sizeof(flag));
	if (ret == -1)
	{
		std::cout << "failed to set socket options!" << std::endl;
	}
	//set localaddr
	local_addr.sin_family = AF_INET;
	local_addr.sin_port = PORT;
	local_addr.sin_addr.s_addr = INADDR_ANY;//0 RENHE IP DOU LISTEN
	bzero(&(local_addr.sin_zero),8);
	//bind socket
	ret = bind(socket_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
	if (ret == -1)
	{
		std::cout << "failed to bind addr!" << std::endl;
		exit(-1);
	}
	//listen
	ret = listen(socket_fd, backlog);
	if (ret == -1)
	{
		std::cout << "failed to listen socket!" << std::endl;
		exit(-1);
	}
	for(;;)
	{
		socklen_t addr_len = sizeof(struct sockaddr);
		accept_fd  = accept(socket_fd, 
							(struct sockaddr *) &remote_addr, 
							&addr_len);						
		pid = fork();
		if (pid == 0)
		{
			for (;;)//loop daozhi zhiyou yige client
			{
				ret = recv(accept_fd, (void *)in_buf, MESSAGE_SIZE, 0);
				if (ret == 0)
					break;
				std::cout << "receive: " << in_buf << std::endl;
		
				send(accept_fd, (void*)in_buf, MESSAGE_SIZE,0);
			}
			close(accept_fd);
		}		
	}
	if (pid!=0)//fu jincheng guanbi
	{
		close(socket_fd);
	}
	return 0;
}

4.1.2 客户端代码

不变

4.2 基于I/O多路复用:select

异步IO,指以事件触发的方式对IO操作进行处理:
如同时响应两个事件

  • 网络客户端发起连接请求
  • 用户在键盘输入命令行

与多进程/多线程相比呢,异步IO技术

  • 系统开销小,不必创建进程/线程,不必维护他们

select方式:

  • 遍历文件描述符集所有描述符,找出有变化的描述符
  • 对于侦听的socket和数据处理的socket区别对待
  • socket设置为非阻塞。

在多进程多线程方式中,接受连接,发起连接,发送接收数据可能是阻塞的因此后面对accept的socket有进行非阻塞式设置

4.2.1基础

fcntl函数
功能描述:根据文件描述词来操作文件的特性。

fcntl函数有5种功能:

  • 复制一个现有的描述符(cmd=F_DUPFD).
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
flags = fcntl(socket_fd, F_GETFL, 0);
fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK);//FEI ZU SE FANGSHI

此处将socket_fd设置为非阻塞式socket

select函数

该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:

#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值:就绪描述符的数目,超时返回0,出错返回-1
  • 第一个参数:待测试的最大描述字加1
  • fd_set是理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
void FD_ZERO(fd_set *fdset);           //清空集合
void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除
 int FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写 
  • timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。(1)永远等待下去(2)等待一段固定时间(3)根本不等待

原理:

在这里插入图片描述

4.2.2 select

第一步

  • 定义accept_fd[FD_SIZE] = {-1,}
  • FD_SIZE默认定义1024,最多1024个连接,弊端

第二步:

  • iocntl设定为非阻塞式

第四步侦听之后:

  • 首先FD_ZERO清空存放文件描述符的集合
  • 然后通过FD_SET将socket_fd加入集合
  • for循环1024.如果每个不等于-1,是一个有效的socket,则将其加入FD_SET(如果大于max_fd替换)
    在这里插入图片描述
    判断select返回值是否>0
  • 判断socket_fd是否可读写,可以的话,找空槽(=-1的),并记录
  • 将接受连接返回的新socket设置为非阻塞
    在这里插入图片描述
  • 如果当前socket有效且可读写,则进接收发送数据
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>//struct
#include <unistd.h>//close head
#include <string.h>//bzero
#include <fcntl.h>//select
#include <stdlib.h>
#define PORT 8111
#define MESSAGE_SIZE 1024
#define FD_SIZE 1024
int main(int argc, char* argv[])
{
	int socket_fd = -1;
	int accept_fd = -1;
	int backlog = 10;
	int ret = -1;
	int flag = 1;
	int maxpos = 0;
	int events = 0;
	int max_fd = -1;
	int curpos = -1;
	int flags;
	fd_set fd_sets;
	int accept_fds[FD_SIZE] = {-1,};
		
	
	struct sockaddr_in local_addr, remote_addr;
	char in_buf[MESSAGE_SIZE] = {0,};
	
	//create socket
	socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (socket_fd == -1)
	{
		std::cout << "file to create socket!" << std::endl;
		exit(-1);
	}

	//set socket options
	ret = setsockopt(socket_fd, 
					SOL_SOCKET, 
					SO_REUSEADDR, 
					&flag, 
					sizeof(flag));
	if (ret == -1)
	{
		std::cout << "failed to set socket options!" << std::endl;
	}
	
		//
	flags = fcntl(socket_fd, F_GETFL, 0);
	fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK);//FEI ZU SE FANGSHI
	
	//
	//set localaddr
	local_addr.sin_family = AF_INET;
	local_addr.sin_port = PORT;
	local_addr.sin_addr.s_addr = INADDR_ANY;//0 RENHE IP DOU LISTEN
	bzero(&(local_addr.sin_zero),8);
	//bind socket
	ret = bind(socket_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
	if (ret == -1)
	{
		std::cout << "failed to bind addr!" << std::endl;
		exit(-1);
	}
	//listen
	ret = listen(socket_fd, backlog);
	if (ret == -1)
	{
		std::cout << "failed to listen socket!" << std::endl;
		exit(-1);
	}
	max_fd = socket_fd;
	  for(int i=0; i< FD_SIZE; i++){
    accept_fds[i] = -1; 
  }  
	for(;;)
	{
		FD_ZERO(&fd_sets);
		FD_SET(socket_fd, &fd_sets);
		
		for (int i = 0; i < maxpos; i++)
		{
			if (accept_fds[i] != -1)
			{
				if (accept_fds[i] > max_fd)
				{
					max_fd = accept_fds[i];
				}
				FD_SET(accept_fds[i], &fd_sets);
			}
		}
		events = select(max_fd+1, &fd_sets, NULL, NULL, NULL);
		if (events < 0)
		{
			std::cout << "failed to use select" << std::endl;
			break;
		}
		else if (events == 0)
		{
			std::cout << "timeout..." << std::endl;
			continue;
		}
		else if (events)
		{
			if (FD_ISSET(socket_fd, &fd_sets))
			{
				for (int i = 0; i < FD_SIZE; i++)
				{
					if (accept_fds[i] == -1)
					{
						curpos = i;
						break;
					}
				}
				socklen_t addr_len = sizeof(struct sockaddr);
				accept_fd  = accept(socket_fd, 
							(struct sockaddr *) &remote_addr, 
							&addr_len);
				flags = fcntl(accept_fd, F_GETFL, 0);
				fcntl(accept_fd, F_SETFL, flags | O_NONBLOCK);//FEI ZU SE FANGSHI
				accept_fds[curpos] = accept_fd;
				
				if(curpos+1 > maxpos){
				  maxpos = curpos + 1; 
				}

				if(accept_fd > max_fd){
				  max_fd = accept_fd; 
				}
			}
			
			for (int i = 0; i < FD_SIZE; i++)
			{
				if (accept_fds[i] != -1 && FD_ISSET(accept_fds[i], &fd_sets))
				{
					memset(in_buf, 0, MESSAGE_SIZE);
					ret = recv(accept_fds[i], (void *)in_buf, MESSAGE_SIZE, 0);
					if (ret == 0)
					{
						close(accept_fds[i]);
						accept_fds[i] = -1;
						break;
					}
					std::cout << "receive: " << in_buf << std::endl;	
					send(accept_fds[i], (void*)in_buf, MESSAGE_SIZE,0);
				}
			}
			
		}	
	}
	close(socket_fd);
	return 0;
}

基于上面的讨论,可以轻松得出select模型的特点

(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。但调整上限受于编译内核时的变量值。

(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd

  • 一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
  • 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个 参数。

可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生)。

缺点:

  • 文件符默认1024,数量太少
  • 半自动方式,找到真正触发的符
  • 但仍然比fork高效

4.3 异步IO :epoll方式

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

  • 没有文件描述符限制
  • 工作效率不会随着文件描述符增加而下降
  • epoll经过内核级的系统优化,更搞笑

4.3.1 epoll触发方式

  • Level Trigger 没有处理反复发送 (水平触发开发难度小)
  • Edge Trigger 只发送一次 (边缘触发开发难度大)

4.3.2 epoll API

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll详解

epoll_ctl:第二个参数:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数:epoll事件:
在这里插入图片描述

4.3.3 epoll工作模式

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

  • LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
  • ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

4.3.4 epoll代码:

Linux高性能服务器

  • 0
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值