20.TCP连接与断开

20.TCP连接与断开

学习目标

▶掌握三次握手建立连接过程

▶掌握四次握手关闭连接的过程

▶掌握滑动窗口的概念

▶掌握错误处理函数封装

▶实现多进程并发服务器

▶实现多线程并发服务器

▶熟练掌握TCP状态转换图

▶熟练掌握端口复用的方法

▶了解半关闭的概念和实现方式

▶了解多路IO转接模型

▶熟练掌握select函数的使用

▶熟练使用fd_set相关函数的使用

▶能够编写select多路IO转接模型的代码

1.三次握手和四次挥手

  思考: 为什么TCP是面向连接的安全可靠的传输????

  TCP是面向连接的安全的数据传输, 在客户端与服务端建立建立的时候要经过三次握手的过程,在客户端与服务端断开连接的时候要经历四次挥手的过程,下图是客户端与服务端三次握手建立连接,数据传输和断开连接四次挥手的全过程。

  TCP时序:

说明讲义中图的含义。

  SYN:表示请求,ACK:表示确认

  服务端发送的SYN和客户端发送的SYN本身也会占1位。

单独讲解三次握手过程,以图解形式说明。

  上图中ACK表示确认序号,确认序号的值是对方发送的序号值+数据的长度,特别注意的是SYN和FIN本身也会占用一位。

  注: SYS----->synchronous

   ACK----->acknowledgement

   FIN------>finish

三次握手和四次挥手的过程都是在内核实现的。

下图是TCP数据报格式

窗口大小:指的是缓冲区大小

通信的时候不再需要SYN标识位了,只有在请求连接的时候需要SYN标识位。

传输数据的时候的随机序号seq就是最近一次对方发送给自己的ACK的随机序号值,而发给对方的ACK就是上次刚刚发给对方的ACK的值。

  图中发送的ACK确认包表示给对方发送数据的一个确认,表示你发送的数据我都收到了,同时告诉对方下次发送该序号开始的数据。

  由于每次发送数据都会收到对方发来的确认包,所以可以确认对方是否收到了,若没有收到对方发来的确认包,则会进行重发。

  mss: 最大报文长度,告诉对方我这边最多一次能收多少,你不能超过这个长度。

  win: 表示告诉对方我这边缓存大小最大是多少。

1.1三次握手

第一种回答

刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态,进行三次握手:

  • 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_SEND 状态。

    首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。

  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。

    在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。

  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

    确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。

在socket编程中,客户端执行connect()时,将触发三次握手。

第二种回答

  • 初始状态:客户端处于 closed(关闭)状态,服务器处于 listen(监听) 状态。
  • 第一次握手:客户端发送请求报文将 SYN = 1同步序列号和初始化序列号seq = x发送给服务端,发送完之后客户端处于SYN_Send状态。(验证了客户端的发送能力和服务端的接收能力)
  • 第二次握手:服务端受到 SYN 请求报文之后,如果同意连接,会以自己的同步序列号SYN(服务端) = 1、初始化序列号 seq = y和确认序列号(期望下次收到的数据包)ack = x+ 1 以及确认号ACK = 1报文作为应答,服务器为SYN_Receive状态。(问题来了,两次握手之后,站在客户端角度上思考:我发送和接收都ok,服务端的发送和接收也都ok。但是站在服务端的角度思考:哎呀,我服务端接收ok,但是我不清楚我的发送ok不ok呀,而且我还不知道你接受能力如何呢?所以老哥,你需要给我三次握手来传个话告诉我一声。你要是不告诉我,万一我认为你跑了,然后我可能出于安全性的考虑继续给你发一次,看看你回不回我。)
  • 第三次握手: 客户端接收到服务端的 SYN + ACK之后,知道可以下次可以发送了下一序列的数据包了,然后发送同步序列号 ack = y + 1和数据包的序列号 seq = x + 1以及确认号ACK = 1确认包作为应答,客户端转为established状态。(分别站在双方的角度上思考,各自ok)

1.2四次挥手相关内容


2.滑动窗口

主要作用: 滑动窗口主要是进行流量控制的。

见下图:如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会导致接收缓冲区满而丢失数据。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是很不同的。

图中win表示告诉对方我这边缓冲区大小是多少,mss表示告诉对方我这边最多一次可以接收多少数据,你最好不要超过这个长度。

在客户端给服务端发包的时候,不一定是非要等到服务端返回响应包,由于客户端知道服务端的窗口大小,所以可以持续多次发送,当发送数据达到对方窗口大小了就不再发送,需要等到对方进行处理,对方处理之后可继续发送。

mss和MTU

MTU:最大传输单元

MTU:通信术语最大传输单元(Maximum Transmission Unit,MTU)

是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为 单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡、串口等),这个值如果设置为太大会导致丢包重传的时候重传的数据量较大,图中的最大值是1500,其实是一个经验值。

mss: 最大报文长度,只是在建立连接的时候,告诉对方我最大能够接收多少数据,在数据通信的过程中就没有mss了。

3.函数封装思想

函数封装的思想-处理异常情况

 结合man-page和errno进行封装。

 在封装的时候起名可以把第一个函数名的字母大写,如socket可以封装成Socket, 这样可以按shift+k进行搜索,shift+k搜索函数说明的时候不区分大小写,使用man page也可以查看,man page对大小写不区分。

像accept,read这样的能够引起阻塞的函数,若被信号打断,由于信号的优先级较高, 会优先处理信号, 信号处理完成后,会使accept或者read解除阻塞,然后返回,此时返回值为 -1,设置errno=EINTR;

errno=ECONNABORTED表示连接被打断,异常。

errno宏:

/usr/include/asm-generic/errno.h文件中包含了errno所有的宏和对应的错误描述信息。

粘包的概念

 粘包:多次数据发送,收尾相连,接收端接收的时候不能正确区分第一次发送多少,第二次发送多少。

 粘包问题分析和解决??

 方案1: 包头+数据

   如4位的数据长度+数据 -----------> 00101234567890

   其中0010表示数据长度,1234567890表示10个字节长度的数据。

   另外,发送端和接收端可以协商更为复杂的报文结构,这个报文结构就相当于双方约定的一个协议。

 方案2: 添加结尾标记。

   如结尾最后一个字符为\n $等。

 方案3: 数据包定长
   如发送方和接收方约定,每次只发送128个字节的内容,接收方接收定长128个字节就可以了。

wrap.c代码解读和分析。

 要求能看懂代码,会使用即可。

4.高并发服务器

如何支持多个客户端---支持多并发的服务器

 由于accept和read函数都会阻塞,如当read的时候,不能调用accept接受新的连接,当accept阻塞等待的时候不能read读数据。

while(1)
{
	cfd = accept();
	
	while(1)
	{
		n = read(cfd, buf, sizeof(buf));
		if(n<=0)
		{
			break;
		}
	}
}

 第一种方案: 使用多进程,可以让父进程接受新连接,让子进程处理与客户端通信

 思路: 让父进程accept接受新连接,然后fork子进程,让子进程处理通信,子进程处理完成后退出,父进程使用SIGCHLD信号回收子进程。

 代码实现:

 第二种方案: 使用多线程,让主线程接受新连接,让子线程处理与客户端通信;使用多线程要将线程设置为分离属性,让线程在退出之后自己回收资源。

思考: 如何不使用多进程或者多线程完成多个客户端的连接请求

 可以将accept和read函数设置为非阻塞,调用fcntl函数可以将文件描述符设置为非阻塞,让后再while循环中忙轮询。假如有多个客户端连接请求,cfd只会保留最后一个文件描述符的值

处理流程:
1 创建socket,得到一个监听的文件描述符lfd---socket()
2 将lfd和IP和端口port进行绑定-----bind();
3 设置监听----listen()
4 进入while(1)

while (1)
{
    //等待有新的客户端连接到来
    cfd = accept();
    //fork一个子进程,让子进程去处理数据
    pid = fork();
    if (pid < 0)
    {
        exit(-1);
    }
    else if (pid > 0)
    {
        //关闭通信文件描述符cfd
        close(cfd);
    }
    else if (pid == 0)
    {
        //关闭监听文件描述符
        close(lfd);

        //收发数据
        while (1)
        {
            //读数据
            n = read(cfd, buf, sizeof(buf));
            if (n <= 0)
            {
                break;
            }

            //发送数据给对方
            write(cfd, buf, n);
        }

        close(cfd);

        //下面的exit必须有,防止子进程再去创建子进程
        exit(0);
    }
}
close(lfd);

这段代码是一个简单的服务端程序,用于接受客户端的连接请求,为每个连接创建一个子进程来处理通信。以下是代码的执行逻辑:

  1. while (1) 循环:这是一个无限循环,表示服务器会一直监听客户端的连接请求。

  2. cfd = accept();accept 函数用于等待并接受客户端的连接请求,返回一个新的通信文件描述符 cfd,该文件描述符用于后续的通信。服务器会在这里等待客户端的连接。

  3. pid = fork();:在这里,服务器为每个客户端连接请求创建一个子进程,以便并发处理多个客户端。如果 fork 失败(pid < 0),则退出程序。

  4. pid > 0 分支:这是父进程分支,父进程会关闭通信文件描述符 cfd,因为父进程不负责具体的数据通信。

  5. pid == 0 分支:这是子进程分支,子进程会在这里处理具体的数据通信。首先,它关闭监听文件描述符 lfd,因为子进程不再需要监听新的连接。然后,进入一个无限循环,不断读取从客户端发送过来的数据(使用 read 函数),然后将数据发送回客户端(使用 write 函数)。

  6. 如果 read 函数返回的字节数 n 小于等于0,表示连接已关闭或发生了错误,子进程会退出循环,关闭通信文件描述符 cfd,然后通过 exit(0) 退出子进程。这个步骤保证了子进程在完成通信后会正常退出。

  7. close(lfd);:在父进程中,当子进程退出后,父进程继续执行,并在退出之前关闭监听文件描述符 lfd。这样做可以确保服务器的监听套接字在程序退出前被正常关闭。

总的来说,这段代码实现了一个基本的多进程服务器,通过不断接受客户端的连接请求,为每个客户端连接创建一个独立的子进程,子进程负责处理具体的数据通信。这种方式能够实现并发处理多个客户端连接。

还需要添加的功能: 父进程使用SIGCHLD信号完成对子进程的回收
注意点: accept或者read函数是阻塞函数,会被信号打断,此时不应该视为一个错误。
errno=EINTR

多进程版本的服务器代码实现:

//多进程版本的网络服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "wrap.h"

int main()
{
	//创建socket
	int lfd = Socket(AF_INET, SOCK_STREAM, 0);
	
	//绑定
	struct sockaddr_in serv;
	bzero(&serv, sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);//表示使用本地任意可用IP
	Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
	
	//设置监听
	Listen(lfd, 128);
	
	pid_t pid;
	int cfd;

	while(1)
	{
		//接受新的连接
		cfd = Accept(lfd, NULL, NULL);
		
		//接受一个新的连接,创建一个子进程,让子进程完成数据的收发操作
		pid = fork();
		if(pid<0)
		{
			perror("fork error");
			exit(-1);
		}
		else if(pid>0)
		{
			//关闭通信文件描述符cfd
			close(cfd);			
		}
		else if(pid==0)
		{
			//关闭监听文件描述符
			close(lfd);
			
			int i=0;
			int n;
			char buf[1024];
			
			while(1)
			{
				//读数据
				n = Read(cfd, buf, sizeof(buf));
				if(n<=0)
				{
					printf("read error or client closed, n==[%d]\n", n);
					break;
				}
				
				//将小写转换为大写
				for(i=0; i<n; i++)
				{
					buf[i] = toupper(buf[i]);
				}
				//发送数据
				Write(cfd, buf, n);
			}
			
			//关闭cfd
			close(cfd);
			exit(0);
		}
	}
	
	//关闭监听文件描述符
	close(lfd);
	
	return 0;
}

这段代码实现了一个简单的多进程版本的网络服务器。以下是代码的主要逻辑:

  1. 创建套接字

    • 使用 Socket 函数创建一个套接字 lfd,用于监听客户端的连接请求。
  2. 绑定地址

    • 使用 Bind 函数将套接字与服务器地址结构 serv 绑定,指定了服务器的 IP 地址和端口号。
  3. 设置监听

    • 使用 Listen 函数设置套接字为监听状态,指定了连接队列的最大长度。
  4. 接受连接

    • 使用 Accept 函数等待并接受客户端的连接请求,返回一个新的通信文件描述符 cfd,用于后续的数据通信。
  5. 创建子进程

    • 使用 fork 函数创建一个子进程,父进程负责接受新的连接,而子进程负责具体的数据通信。
    • 如果 fork 失败,输出错误信息并退出。
  6. 父进程处理

    • 在父进程中,关闭通信文件描述符 cfd,因为父进程不负责具体的数据通信。
  7. 子进程处理

    • 在子进程中,关闭监听文件描述符 lfd,因为子进程不再需要监听新的连接。
    • 进入一个无限循环,不断地读取从客户端发送过来的数据,将数据中的小写字母转换为大写,并发送回客户端。
    • 如果读取到的字节数 n 小于等于0,表示连接已关闭或发生了错误,子进程退出循环,关闭通信文件描述符 cfd,然后通过 exit(0) 正常退出子进程。
  8. 主循环

    • 返回到主循环,继续等待新的连接请求,重复上述过程。
  9. 关闭监听套接字

    • 在程序结束前,关闭监听套接字 lfd

总体来说,该程序实现了一个简单的多进程网络服务器,能够并发处理多个客户端连接,每个连接都有一个独立的子进程负责数据通信。这种模型适用于较为简单的网络服务场景。

wrap.c

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.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;

	if ((n = connect(fd, sa, salen)) < 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;              //usigned int 剩余未读取的字节数
	ssize_t nread;              //int 实际读到的字节数
	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;
}

int tcp4bind(short port,const char *IP)
{
    struct sockaddr_in serv_addr;
    int lfd = Socket(AF_INET,SOCK_STREAM,0);
    bzero(&serv_addr,sizeof(serv_addr));
    if(IP == NULL){
        //如果这样使用 0.0.0.0,任意ip将可以连接
        serv_addr.sin_addr.s_addr = INADDR_ANY;
    }else{
        if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){
            perror(IP);//转换失败
            exit(1);
        }
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port   = htons(port);
    Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
    return lfd;
}

wrap.h

#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.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);
int tcp4bind(short port,const char *IP);
#endif

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ gcc -o server 01-mult-process.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ ./server

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ netstat -anp | grep 8888
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN      21746/./server      

0 0.0.0.0是本机的意思,只能绑定本机。

终端3

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ nc 127.1 8888
nihaoxiaowu
NIHAOXIAOWU

终端4

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ nc 127.1 8888
quba
QUBA

改进:

//多进程版本的网络服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "wrap.h"

int main()
{
	//创建socket
	int lfd = Socket(AF_INET, SOCK_STREAM, 0);
	
	//绑定
	struct sockaddr_in serv;
	bzero(&serv, sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
	
	//设置监听
	Listen(lfd, 128);
	
	pid_t pid;
	int cfd;
	char sIP[16];
	socklen_t len;
	struct sockaddr_in client;
	while(1)
	{
		//接受新的连接
		len = sizeof(client);
		memset(sIP, 0x00, sizeof(sIP));
		cfd = Accept(lfd, (struct sockaddr *)&client, &len);
		printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));
		
		//接受一个新的连接, 创建一个子进程,让子进程完成数据的收发操作
		pid = fork();
		if(pid<0)
		{
			perror("fork error");
			exit(-1);
		}
		else if(pid>0)
		{
			//关闭通信文件描述符cfd
			close(cfd);			
		}
		else if(pid==0)
		{
			//关闭监听文件描述符
			close(lfd);
			
			int i=0;
			int n;
			char buf[1024];
			
			while(1)
			{
				//读数据
				n = Read(cfd, buf, sizeof(buf));
				if(n<=0)
				{
					printf("read error or client closed, n==[%d]\n", n);
					break;
				}
				//printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));
				printf("[%d]---->:n==[%d], buf==[%s]\n", ntohs(client.sin_port), n, buf);
				
				//将小写转换为大写
				for(i=0; i<n; i++)
				{
					buf[i] = toupper(buf[i]);
				}
				//发送数据
				Write(cfd, buf, n);
			}
			
			//关闭cfd
			close(cfd);
			exit(0);
		}
	}
	
	//关闭监听文件描述符
	close(lfd);
	
	return 0;
}

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ gcc -o server 01-mult-process.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ ./server

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ netstat -anp | grep 8888
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        0      0 127.0.0.1:44802         127.0.0.1:8888          TIME_WAIT   -                   
tcp        0      0 192.168.83.129:8888     192.168.83.1:3685       TIME_WAIT   -                   

终端3

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ nc 127.1 8888

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ gcc -o server 01-mult-process.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ ./server
client:[127.0.0.1] [37054]

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ gcc -o server 01-mult-process.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ ./server

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ nc 127.1 8888

终端3

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ nc 127.1 8888

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ gcc -o server 01-mult-process.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ ./server
client:[127.0.0.1] [50336]
client:[127.0.0.1] [32824]

终端4

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ netstat -anp | grep 8888
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN      25373/./server      
tcp        0      0 127.0.0.1:8888          127.0.0.1:32824         ESTABLISHED 25378/./server      
tcp        0      0 127.0.0.1:32824         127.0.0.1:8888          ESTABLISHED 25377/nc            
tcp        0      0 127.0.0.1:8888          127.0.0.1:50336         ESTABLISHED 25376/./server      
tcp        0      0 127.0.0.1:50336         127.0.0.1:8888          ESTABLISHED 25375/nc

2个链接对

父子进程能够共享的:
 文件描述符(子进程复制父进程的文件描述符)
 mmap共享映射区

多线程版本的服务器开发流程:

{
	1 创建socket, 得到一个监听的文件描述符lfd---socket()
	2 将lfd和IP和端口port进行绑定-----bind();
	3 设置监听----listen() 
	4 while(1)
	  {
	  	//接受新的客户端连接请求
	  	cfd = accept();
	  	
	  	//创建一个子线程
	  	pthread_create(&threadID, NULL, thread_work, &cfd);
	  	
	  	//设置线程为分离属性
	  	pthread_detach(threadID);
	  	
	  }
	  
	  close(lfd);
}

子线程执行函数:

void* thread_work(void* arg)
{
	//获得参数: 通信文件描述符
	int cfd = *(int*)arg;

	while (1)
	{
		//读数据
		n = read(cfd, buf, sizeof(buf));
		if (n <= 0)
		{
			break;
		}

		//发送数据
		write(cfd, buf, n);
	}

	close(cfd);
}

问题:
 1.子线程能否关闭lfd?
  子线程不能关闭监听文件描述符lfd,原因是子线程和主线程共享文件描述符,而不是复制的。
 2.主线程能否关闭cfd?
  主线程不能关闭cfd,主线程和子线程共享一个cfd,而不是复制的,close之后cfd就会被真正关闭。
 3.多个子线程共享cfd,会有什么问题发生?

02-mult-thread.c

//多线程版本的高并发服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include <pthread.h>
#include "wrap.h"

//子线程回调函数
void *thread_work(void *arg)
{
	sleep(20);
	int cfd = *(int *)arg;
	printf("cfd==[%d]\n", cfd);
	
	int i;
	int n;
	char buf[1024];
	
	while(1)
	{
		//read数据
		memset(buf, 0x00, sizeof(buf));
		n = Read(cfd, buf, sizeof(buf));
		if(n<=0)
		{
			printf("read error or client closed,n==[%d]\n", n);
			break;
		}
		printf("n==[%d], buf==[%s]\n", n, buf);
		
		for(i=0; i<n; i++)
		{
			buf[i] = toupper(buf[i]);
		}
		//发送数据给客户端
		Write(cfd, buf, n);	
	}
	
	//关闭通信文件描述符
	close(cfd);
	
	pthread_exit(NULL);
}

int main()
{
	//创建socket
	int lfd = Socket(AF_INET, SOCK_STREAM, 0);
	
	//设置端口复用
	//int opt = 1;
	//setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
	
	//绑定
	struct sockaddr_in serv;
	bzero(&serv, sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
	
	//设置监听
	Listen(lfd, 128);
	
	int cfd;
	pthread_t threadID;
	while(1)
	{
		//接受新的连接
		cfd = Accept(lfd, NULL, NULL);
		
		//创建子线程
		pthread_create(&threadID, NULL, thread_work, &cfd);
		
		//设置子线程为分离属性
		pthread_detach(threadID);
	}

	//关闭监听文件描述符
	close(lfd);
	
	return 0;
}

wrap.c

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.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;

	if ((n = connect(fd, sa, salen)) < 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;              //usigned int 剩余未读取的字节数
	ssize_t nread;              //int 实际读到的字节数
	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;
}

int tcp4bind(short port,const char *IP)
{
    struct sockaddr_in serv_addr;
    int lfd = Socket(AF_INET,SOCK_STREAM,0);
    bzero(&serv_addr,sizeof(serv_addr));
    if(IP == NULL){
        //如果这样使用 0.0.0.0,任意ip将可以连接
        serv_addr.sin_addr.s_addr = INADDR_ANY;
    }else{
        if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){
            perror(IP);//转换失败
            exit(1);
        }
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port   = htons(port);
    Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
    return lfd;
}

wrap.h

#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.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);
int tcp4bind(short port,const char *IP);
#endif

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ gcc -o multithread 02-mult-thread.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ ./multithread 

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ netstat -anp | grep 8888
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN      26088/./multithread 

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ netstat -anp | grep 8888
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN      26088/./multithread 
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ nc 127.1 8888
nihao
NIHAO

终端3

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ nc 127.1 8888
nihaoya
NIHAOYA


终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ gcc -o multithread 02-mult-thread.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ ./multithread 
n == [6], buf == [nihao
]
n == [8], buf == [nihaoya
]

02-mult-thread.c

//多线程版本的高并发服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include <pthread.h>
#include "wrap.h"

int g_lfd;

//子线程处理函数
void *thread_work(void *arg)
{
	int cfd = *(int *)arg;
	printf("cfd == [%d]\n", cfd);

	close(g_lfd);

	char buf[1024];
	int n;
	int i;

	while(1)
	{
		//读数据
		memset(&buf, 0x00, sizeof(buf));
		n = Read(cfd, buf, sizeof(buf));
		if(n <= 0)
		{
			printf("read error or client closed, n == [%d]\n", n);
			break;
		}
		printf("cfd == [%d], buf == [%s]\n", cfd, buf);

		//将大写字母转换成小写字母
		for(i = 0; i < n; i++)
		{
			buf[i] = toupper(buf[i]);
		}

		//发送数据
		Write(cfd, buf, n);
	}

	//关闭通信文件描述符
	close(cfd);

	pthread_exit(NULL);
}

int main()
{
	//创建socket
	int lfd = Socket(AF_INET, SOCK_STREAM, 0);
	g_lfd = lfd;

	//绑定
	struct sockaddr_in serv;
	bzero(&serv, sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);//表示使用本地任意可用字符
	Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));

	//设置监听
	Listen(lfd, 128);

	int cfd;
	pthread_t threadID;

	while(1)
	{
		//接受新的连接
		cfd = Accept(lfd, NULL, NULL);
		
		//创建子线程
		pthread_create(&threadID, NULL, thread_work, &cfd);

		//设置子线程为分离属性
		pthread_detach(threadID);
	}

	//关闭监听文件描述符
	close(lfd);

	return 0;
}

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ gcc -o multithread 02-mult-thread.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ ./multithread 

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ nc 127.1 8888

终端2输入ssddd

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ nc 127.1 8888
ssddd
SSDDD

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ gcc -o multithread 02-mult-thread.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ ./multithread 
cfd == [4]
cfd == [4], buf == [ssddd
]

终端3

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ nc 127.1 8888

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ gcc -o multithread 02-mult-thread.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ ./multithread 
cfd == [4]
cfd == [4], buf == [ssddd
]
accept error: Bad file descriptor
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ 

监听文件描述符已经被关闭了

问题:
 1.子线程能否关闭lfd?
  子线程不能关闭监听文件描述符lfd,原因是子线程和主线程共享文件描述符,而不是复制的。
 2.主线程能否关闭cfd?
  主线程不能关闭cfd,主线程和子线程共享一个cfd,而不是复制的,close之后cfd就会被真正关闭。
 3.多个子线程共享cfd,会有什么问题发生?

5.TCP状态转换图

了解TCP状态转换图可以帮助开发人员查找问题。

说明: 上图中粗线表示主动方,虚线表示被动方,细线部分表示一些特殊情况,解即可,不必深入研究。

  对于建立连接的过程客户端属于主动方,服务端属于被动接受方(图的上半部分)

  而对于关闭(图的下半部分),服务端和客户端都可以先进行关闭。

  处于ESTABLISHED状态的时候就可以收发数据了,双方在通信过程当中一直处于ESTABLISHED状态,数据传输期间没有状态的变化。

  TIME_WAIT状态一定是出现在主动关闭的一方。

  主动关闭的Socket端会进入TIME_WAIT状态,并且持续2MSL时间长度,MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。

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可用状态。

使用netstat -anp可以查看连接状态

注:数据传输的时候带了一个字节的数据,所以server发送给client的ACK=x+2

为什么需要2MSL?

  原因之一:让四次挥手的过程更可靠,确保最后一个发送给对方的ACK到达;若对方没有收到ACK应答,对方会再次发送FIN请求关闭,此时在2MSL时间内被动关闭方仍然可以发送ACK给对方。

  原因之二:为了保证在2MSL时间内,不能启动相同的SOCKET-PAIR。TIME_WAIT一定是出现在主动关闭的一方,也就是说2MSL是针对主动关闭一方来说的;由于TCP有可能存在丢包重传,丢包重传若发给了已经断开连接之后相同的socket-pair(该连接是新建的,与原来的socket-pair完全相同,双方使用的是相同的IP和端口),这样会对之后的连接造成困扰,严重可能引起程序异常。

如何避免问题2呢??

  --很多操作系统实现的时候,只要端口被占用,服务就不能启动。

测试:启动服务端和客户端,然后先关闭服务端,再次启动服务端,此时服务端报错:bind error: Address already in use; 若是先关闭的客户端,再关闭的服务端,此时启动服务端就不会报这个错误。

socket-pair的概念:客户端与服务端连接其实是一个连接对,可以通过使用netstat -anp | grep 端口号进行查看。

01-mult-process.c

//多进程版本的网络服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "wrap.h"

int main()
{
	//创建socket
	int lfd = Socket(AF_INET, SOCK_STREAM, 0);
	
	//绑定
	struct sockaddr_in serv;
	bzero(&serv, sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
	
	//设置监听
	Listen(lfd, 128);
	
	pid_t pid;
	int cfd;
	char sIP[16];
	socklen_t len;
	struct sockaddr_in client;
	while(1)
	{
		//接受新的连接
		len = sizeof(client);
		memset(sIP, 0x00, sizeof(sIP));
		cfd = Accept(lfd, (struct sockaddr *)&client, &len);
		printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));
		
		//接受一个新的连接, 创建一个子进程,让子进程完成数据的收发操作
		pid = fork();
		if(pid<0)
		{
			perror("fork error");
			exit(-1);
		}
		else if(pid>0)
		{
			//关闭通信文件描述符cfd
			close(cfd);			
		}
		else if(pid==0)
		{
			//关闭监听文件描述符
			close(lfd);
			
			int i=0;
			int n;
			char buf[1024];
			
			while(1)
			{
				//读数据
				n = Read(cfd, buf, sizeof(buf));
				if(n<=0)
				{
					printf("read error or client closed, n==[%d]\n", n);
					break;
				}
				//printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));
				printf("[%d]---->:n==[%d], buf==[%s]\n", ntohs(client.sin_port), n, buf);
				
				//将小写转换为大写
				for(i=0; i<n; i++)
				{
					buf[i] = toupper(buf[i]);
				}
				//发送数据
				Write(cfd, buf, n);
			}
			
			//关闭cfd
			close(cfd);
			exit(0);
		}
	}
	
	//关闭监听文件描述符
	close(lfd);
	
	return 0;
}

wrap.h

#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.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);
int tcp4bind(short port,const char *IP);
#endif

wrap.c

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.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;

	if ((n = connect(fd, sa, salen)) < 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;              //usigned int 剩余未读取的字节数
	ssize_t nread;              //int 实际读到的字节数
	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;
}

int tcp4bind(short port,const char *IP)
{
    struct sockaddr_in serv_addr;
    int lfd = Socket(AF_INET,SOCK_STREAM,0);
    bzero(&serv_addr,sizeof(serv_addr));
    if(IP == NULL){
        //如果这样使用 0.0.0.0,任意ip将可以连接
        serv_addr.sin_addr.s_addr = INADDR_ANY;
    }else{
        if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){
            perror(IP);//转换失败
            exit(1);
        }
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port   = htons(port);
    Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
    return lfd;
}

没有连接,关闭后可以启动

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ nc 127.1 8888
ssssdfdd
SSSSDFDD

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ gcc -o mult 01-mult-process.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ ./mult
client:[127.0.0.1] [54902]
[54902]---->:n==[9], buf==[ssssdfdd

关闭客户端(终端2),终端1可以启动起来

启动客户端(终端2),再启动终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day10$ ./mult
bind error: Address already in use

TCP状态转换图:
1.三次握手过程:
 客户端:SYN_SENT---connect()
 服务端:LISTEN--listen() SYN_RCVD
 当三次握手完成后,都处于ESTABLISHED状态

2.数据传输过程中状态不发生变化,都是ESTABLISHED状态

3.四次挥手过程:
 主动关闭方:FIN_WAIT_T FIN_WAIT_2 TIME_WAIT
 被动关闭方:CLOSE_WAIT LAST_ACK

思考题?
1.SYN_SENT状态出现在哪一方? 客户端
2.SYN_RCVD状态出现在哪一方? 服务端
3.TIME_WAIT状态出现在哪一方? 主动关闭方
4.在数据传输的时候没有状态变化。

TIME_WAIT是如何出现的:
启动服务端,启动客户端,连接建好,而且也可以正常发送数据;然后先关闭服务端,服务端就会出现TIME_WAIT状态。

6.端口复用

解决端口复用的问题:bind error: Address already in use,发生这种情况是在服务端主动关闭连接以后,接着立刻启动就会报这种错误。

setsockopt函数

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(int));

函数说明可参看<<UNIX环境高级编程>>

由于错误是bind函数报出来的,该函数调用要放在bind之前,socket之后调用。

参数
sockfd:要设置的套接字描述符。监听的文件描述符。
level:选项定义的层次。或为特定协议的代码(如IPv4,IPv6,TCP,SCTP),或为通用套接字代码(SOL_SOCKET)。
optname:选项名。level对应的选项,一个level对应多个选项,不同选项对应不同功能。
optval:指向某个变量的指针,该变量是要设置新值的缓冲区。可以是一个结构体,也可以是普通变量
optlen:optval的长度。

设置端口复用:

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

报错是bind,所以在bind之前

7.半关闭状态

半关闭的概念:

 如果一方close,另一方没有close,则认为是半关闭状态,处于半关闭状态的时候,可以接收数据,但是不能发送数据。相当于把文件描述符的写缓冲区操作关闭了。

 注意:半关闭一定是出现在主动关闭的一方。

shutdown函数

 长连接和端连接的概念:

  连接建立之后一直不关闭为长连接;

  连接收发数据完毕之后就关闭为短连接;

shutdown和close的区别:

 shutdown能够把文件描述符上的读或者写操作关闭,而close关闭文件描述符只是将连接的引用计数的值减1,当减到0就真正关闭文件描述符了。

 如:调用dup函数或者dup2函数可以复制一个文件描述符,close其中一个并不影响另一个文件描述符,而shutdown就不同了,一旦shutdown了其中一个文件描述符,对所有的文件描述符都有影响。

8.心跳包

如何检查与对方的网络连接是否正常??

一般心跳包用于长连接。

方法1

keepAlive = 1;

setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));

由于不能实时的检测网络情况,一般不用这种方法

方法2:在应用程序中自己定义心跳包,使用灵活,能实时把控。

什么是心跳包?
用于监测长连接是否正常的字符串。
在什么情况下使用心跳包?
主要用于监测长连接是否正常。
如何使用心跳包?
通信双方需要协商规则(协议),如4个字节长度+数据部分

9.高并发服务器模型--select 主要用于服务端

继续研究高并发服务器的问题。

IO多路复用底层原理就是委托内核去检测在通信过程中一系列文件描述符的状态,文件描述符都对应两块缓冲区分别为读和写,内核的检测就解放了accept函数、read/recv、write/send函数。这些函数在调用时不需要再对这些缓冲区做是不是可用做检测,我们只需要调用这些函数,这些函数就能正常被执行,不需要被阻塞。

IO多路复用的第一种处理方式就是select,这个函数可以跨平台使用。

文件描述符分为两类一类是监听的另外一类是通信的文件描述符,服务器端监听的文件描述符只有一个,通信的文件描述符有n个。每建立一个新连接都会得到一个新的用于通信的文件描述符。能够通过select监控的文件描述符是监听加通信一共n+1个文件描述符,这些文件描述符读缓冲区和谐缓冲区都可以基于select来进行检测。

多路IO技术: select,同时监听多个文件描述符,将监控的操作交给内核去处理

数据类型fd_set:文件描述符集合--本质是位图(关于集合可联想一个信号集sigset_t)

int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

函数介绍:委托内核监控该文件描述符对应的读,写或者错误事件的发生。

参数说明:

 nfds: 告诉内核要监控的文件描述符的范围,一般取值最大的文件描述符+1

 readfds: 读集合,是一个传入传出参数

  输入参数: 指的是告诉内核哪些文件描述符需要监控

  输出参数: 指的是内核告诉应用程序哪些文件描述符发生了变化

 writefds: 写文件描述符集合(传入传出参数)

  输入参数: 指的是告诉内核哪些文件描述符需要监控

  输出参数: 指的是内核告诉应用程序哪些文件描述符发生了变化

 execptfds: 异常文件描述符集合(传入传出参数),输入输出参数,表示一般异常事件

 timeout:

  NULL--表示永久阻塞,直到有事件发生

  0 --表示不阻塞,立刻返回,不管是否有监控的事件发生

  >0--表示阻塞的时长,若没有超过时长,则一直阻塞;若在时长内,有事件发生,则立刻返回

返回值: 成功返回发生变化的文件描述符的个数

 失败返回-1,并设置errno值。

nfds是要检测的文件描述符集合里最大的文件描述符的值+1,要检测的集合就是后面几个参数,第一个是读集合,读集合表示要检测一系列文件描述符集合的读缓冲区,写集合表示要检测一系列文件描述符集合的写缓冲区,异常集合表示要检测一系列文件描述符集合是否有异常,有异常就会触发一个异常事件。第二个第三个和第四个参数是地址,因为需要把一块内存传递给select,select需要在函数内修改这块内存地址里的数据,如果需要委托select检测一系列文件描述符的读缓冲区,就把要检测的文件描述符放入读集合中;要检测一系列文件描述符的写缓冲区是否可写就需要把一系列文件描述符加入到写集合中。可以同时检测这一系列的文件描述符的读写异常情况,当把要检测的文件描述符传入select后,内核会把这三个集合拷贝一份,内核就有了这三份数据,内核就基于存储这三个集合的线性表去检测这三个集合的读写异常事件,当内核通过线性遍历把要检测的文件描述符他们对应的读写缓冲区以及异常遍历一遍以后,发现某些文件描述符里面有数据了,或者某些文件描述符可写,某些文件描述符发生了异常,select就会传出对应的三个集合。传出的数据会再次写入到读写异常指针所指向的内存空间中。

nfds是要在检测的文件描述符集合里最大的文件描述符的值加上1的原因。

内部是基于一个线性表来进行文件描述符的状态检测的,因此要把最大的一个文件描述符制指定出来,这个值是文件描述符检测指定的一个标志,如果不告诉最大值就不知道在哪结束。如果不知道ndfs填多少就填1024。

/usr/include/x86_64-linux-gnu/sys/select.h

/usr/include/x86_64-linux-gnu/bits/select.h

从上面的文件中可以看出,这几个宏本质上还是位操作。

void FD_CLR(int fd, fd_set *set);

将fd从set集合中清除。

int FD_ISSET(int fd, fd_set *set);

功能描述: 判断fd是否在集合中

返回值: 如果fd在set集合中,返回1,否则返回0。

void FD_SET(int fd, fd_set *set);

将fd设置到set集合中。

void FD_ZERO(fd_set *set);

初始化set集合。

调用select函数其实就是委托内核帮我们去检测哪些文件描述符有可读数据,可写,错误发生;

如果要检测一系列的文件描述符是有读事件触发,就需要把要检测的文件描述符加入到fd_set(read)读集合中。设置好之后就需要把fd_set(read)传递到内核里,这个过程做了数据拷贝,内核基于这个集合进行一个线性检测。检测的时候会基于这个线性表和内核里的文件描述符表进行对比,因内核里的文件描述符表每个文件描述符都对应两个缓冲区,当做检测的时候,先检测fd3读缓冲区是否有数据,如果读缓冲区有数据就把他记录下来。下图fd3,fd6,fd9,fd10有数据,检测完毕后把满足条件的文件描述符重新写回指定的内存中,参数fd_set(read)对应的那块内存是用户区的内存,传入的时候做了初始化,因为内核要更新fd_set(read)这个集合,更新的时候发生了变化,因有一些文件描述符没有数据,就会把fd_set(read)集合的一些对应标志位修改为0 ,说明是没有数据读入的,然后就可以对fd_set(read)集合进行遍历了。判断一下每个文件描述符对应的标志位是否为1,如果为1,说明它们的读缓冲区是有数据的,再去判断一下这个文件描述符适用于监听的还是通信的,如果是监听的就和客户端进行新的连接,如果是通信的就可以就这个文件描述符去读数据。写集合同理。

什么事就绪状态?

对于读集合来说,读缓冲区有数据就是就绪的文件描述符。对于写集合,如果说writefds这个集合的文件描述符对应的写缓冲区可写,就是就绪状态。对于异常集合,如果exceptfd这个集合的文件描述符有异常就是就绪状态。select函数返回了就会得到一个值,这个值就是读写异常集合的满足条件的文件描述符的个数。

分析:

1.使用select委托内核监控可读事件,可读事件分为两类,一类是有客户端连接请求到来,另一类是有客户端数据发来

2.连接请求是可读事件,因为服务端要调用connect建立连接,调用connect有个三次握手过程,三次握手过程中服务端要发送SYN请求,SYN请求之后要告诉内核,三次握手是内核帮助实现的,内核知道读缓冲区是否有数据,比应用程序先知道,因为数据在内核中。

3.使用select委托内核监控,看有没有客户端连接发过来,对于服务端来说应该监控监听文件描述符,所以将监听文件描述符加入到可读事件当中去

代码思路:

使用select的开发服务端流程:

1.创建socket,得到监听文件描述符lfd---socket()

2.设置端口复用-----setsockopt()

3.将lfd和IP、PORT绑定----bind()

4.设置监听---listen()

5.委托内核监控是否有客户端连接请求

 (1)定义文件描述符集变量:fd_set readfds;

 (2)清空文件描述符集变量:FD_ZERO(&readfds);

 (3)将lfd加入到readfds集合中:FD_SET(lfd, &readfds);

 (4)内核持续监控文件描述符

  ①有客户端连接请求到来:

判断lfd文件描述符是否变化(lfd不在集合中即有变化,也就是有数据发过来)

有的话接受新的客户端连接请求,将cfd加入到readfds集合中,修改内核监控的文件描述符的范围

  ②有客户端数据发来:

判断文件描述符是否在集合中,如果在,一定有数据发送过来,有数据发送过来就去读数据并且应答数据给客户端

fd_set readfds;  //定义文件描述符集变量
fd_set tmpfds;
FD_ZERO(&readfds);  //清空文件描述符集变量
FD_SET(lfd, &readfds);//将lfd加入到readfds集合中
maxfd = lfd;

while (1)
{
    tmpfds = readfds;
    nready = select(maxfd + 1, &tmpfds, NULL, NULL, NULL);
    if (nready < 0)
    {
        if (errno == EINTR)//被信号中断
        {
            continue;
        }
        break;
    }

    //有客户端连接请求到来
    if (FD_ISSET(lfd, &tmpfds))
    {
        //接受新的客户端连接请求
        cfd = accept(lfd, NULL, NULL);

        //将cfd加入到readfds集合中
        FD_SET(cfd, &readfds);

        //修改内核监控的文件描述符的范围
        if (maxfd < cfd)
        {
            maxfd = cfd;
        }

        if (--nready == 0)
        {
            continue;
        }
    }


    //有客户端数据发来
    for (i = lfd + 1; i <= maxfd; i++)
    {
        if (FD_ISSET(i, &tmpfds))
        {
            //read数据
            n = read(i, buf, sizeof(buf));
            if (n <= 0)
            {
                close(i);
                //将文件描述符i从内核中去除
                FD_CLR(i, &readfds);
            }

            //write应答数据给客户端
            write(i, buf, n);
        }

        if (--nready == 0)
        {
            break;
        }
    }

    close(lfd);

    return 0;
}

这段代码是一个使用 select 函数的简单的多路复用服务器,通过 select 实现同时监听多个文件描述符(包括监听套接字和已连接套接字)的读事件,实现了非阻塞的 I/O 操作。

主要逻辑如下:

  1. 初始化文件描述符集合

    • fd_set readfds; 声明了一个文件描述符集合变量。
    • FD_ZERO(&readfds); 清空了文件描述符集合变量。
  2. 将监听套接字加入到文件描述符集合

    • FD_SET(lfd, &readfds); 将监听套接字 lfd 添加到文件描述符集合中。
    • maxfd = lfd; 初始化 maxfd 为监听套接字的值。
  3. 进入主循环

    • tmpfds = readfds; 将文件描述符集合赋值给 tmpfds
    • 使用 select 函数监控文件描述符的读事件。
    • 如果 select 返回小于 0,表示出现错误。如果错误是由于信号中断(EINTR),则继续循环;否则,退出循环。
    • 如果 select 返回大于等于 0,表示有文件描述符就绪。
  4. 监听套接字是否有连接请求

    • 如果 lfdtmpfds 中就绪,表示有新的连接请求到来。

    当 lfd 在 tmpfds 中就绪时,表示有新的连接请求到来,是因为 lfd 是服务器端的监听套接字,它负责监听客户端的连接请求。当有新的连接请求到来时,lfd 就会变为就绪状态,可以通过 select 函数监测到。此时可以调用 accept 函数来接受新的客户端连接请求,并将新的客户端套接字加入到文件描述符集合中,以便后续对新的客户端进行数据读写操作。

    • 使用 accept 函数接受新的客户端连接请求,并将新连接的套接字加入到文件描述符集合中(FD_SET(cfd, &readfds))。
    • 更新 maxfd
  5. 已连接套接字是否有数据可读

    • 遍历从 lfd + 1maxfd 的所有文件描述符。
    • 如果某个文件描述符在 tmpfds 中就绪,表示有数据可读。
    • 使用 read 函数读取数据,如果返回值小于等于 0,表示连接已关闭或发生错误,关闭文件描述符并从文件描述符集合中移除(FD_CLR(i, &readfds))。
    • 使用 write 函数将数据写回客户端。
  6. 关闭监听套接字

    • 在循环外部的 close(lfd) 是不合理的,应该放在程序结束的地方。
  7. 返回

    • return 0; 放在了循环内部,这样在第一次循环时就会退出函数,可能是一个错误。

需要注意的是,这里只是简单示例,实际应用中可能需要更多的错误处理和程序逻辑。

在这个代码中,--nready 的判断主要是为了处理在同一次 select 调用中可能发生的多个事件。

  1. 客户端连接请求

    • 如果 FD_ISSET(lfd, &tmpfds) 为真,表示监听套接字 lfd 有新的连接请求。
    • 执行 accept,并将新连接的套接字 cfd 加入到 readfds 集合中,并更新 maxfd
    • 如果在同一次 select 调用中有多个连接请求,--nready 的作用是确保在处理完一个连接请求后继续检查是否还有其他连接请求。
  2. 客户端数据发来

    • 如果 FD_ISSET(i, &tmpfds) 为真,表示已连接套接字 i 有数据可读。
    • 执行 read 函数读取数据,如果返回值小于等于 0,表示连接已关闭或发生错误,关闭文件描述符,并从 readfds 集合中移除。
    • 如果在同一次 select 调用中有多个套接字有数据可读,--nready 的作用是确保在处理完一个套接字后继续检查是否还有其他套接字有数据可读。

总体来说,--nready 的判断确保在处理完一个事件后,继续检查是否还有其他就绪的事件,以充分利用同一次 select 调用中的多个事件。这是因为 select 的返回值 nready 表示就绪的文件描述符的个数,通过 --nready 可以实现在同一次调用中处理所有就绪的事件。

代码的具体实现:编写代码并进行测试。

01-select.c

//IO多路复用-select
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include <errno.h>
#include <sys/select.h>
#include "wrap.h"

int main()
{
	//创建socket
	int lfd = Socket(AF_INET, SOCK_STREAM, 0);

	//设置端口复用
	int opt = 1;
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

	//绑定
	struct sockaddr_in serv;
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));

	//监听
	Listen(lfd, 128);

	//定义fd_set类型的变量
	fd_set readfds;
	fd_set tmpfds;

	//清空readfds和tmpfds集合
	FD_ZERO(&readfds);
	FD_ZERO(&tmpfds);

	//将lfd加入到readfds中,委托内核监控
	FD_SET(lfd, &readfds);

	int maxfd = lfd;
	int nready;
	int cfd;
	int i;
	int sockfd;
	int n;
	char buf[1024];

	while(1)
	{
		tmpfds = readfds;
		//tmpfds是输入输出参数:
		//输入:告诉内核要监控哪些文件描述符
		//输出:内核告诉应用程序有哪些文件描述符发生了变化
		nready = select(maxfd+1,&tmpfds, NULL, NULL, NULL);
		if(nready < 0)
		{
			if(errno == EINTR)//被信号中断
			{
				continue;
			}
			break;
		}

		//有客户端连接请求到来
		if(FD_ISSET(lfd, &tmpfds))
		{
			//接受新的客户端连接请求
			cfd = Accept(lfd, NULL, NULL);

			//将cfd加入到readfds集合中
			FD_SET(cfd, &readfds);

			//修改内核的监控范围
			if(maxfd < cfd)
			{
				maxfd = cfd;
			}

			if(--nready <= 0)
			{
				continue;
			}
		}

		//有数据发来的情况
		for(i = lfd+1; i <= maxfd; i++)
		{
			sockfd = i;
			//判断sockfd文件描述符是否有变化
			if(FD_ISSET(sockfd, &tmpfds));
			{
				//读数据
				memset(buf, 0x00, sizeof(buf));
				n = Read(sockfd, buf, sizeof(buf));
				if(n <= 0)
				{
					//关闭连接
					close(sockfd);
					
					//将sockfd从readfds中删除
					FD_CLR(sockfd, &readfds);
				}
				else
				{
					printf("n == [%d], buf == [%s]\n", n, buf);

					int k = 0;
					for(k = 0; k < n; k++)
					{
						buf[k] = toupper(buf[k]);
					}

					Write(sockfd, buf, n);
				}

				if(--nready == 0)
				{
					break;
				}
			}
		}

	}

	close(lfd);

	return 0;
}

wrap.h

#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.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);
int tcp4bind(short port,const char *IP);
#endif
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.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;

	if ((n = connect(fd, sa, salen)) < 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;              //usigned int 剩余未读取的字节数
	ssize_t nread;              //int 实际读到的字节数
	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;
}

int tcp4bind(short port,const char *IP)
{
    struct sockaddr_in serv_addr;
    int lfd = Socket(AF_INET,SOCK_STREAM,0);
    bzero(&serv_addr,sizeof(serv_addr));
    if(IP == NULL){
        //如果这样使用 0.0.0.0,任意ip将可以连接
        serv_addr.sin_addr.s_addr = INADDR_ANY;
    }else{
        if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){
            perror(IP);//转换失败
            exit(1);
        }
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port   = htons(port);
    Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
    return lfd;
}

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ gcc -o select 01-select.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ ./select
n == [12], buf == [hello world
]

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ nc 127.1 8888
hello world
HELLO WORLD

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ netstat -anp | grep 8888
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN      10136/./select      
tcp        0      0 127.0.0.1:33400         127.0.0.1:8888          ESTABLISHED 10137/nc            
tcp        0      0 127.0.0.1:8888          127.0.0.1:33400         ESTABLISHED 10136/./select      

 可以使用发生事件的总数进行控制,减少循环次数

 调用select函数设计到了用户空间和内核空间的数值交互过程。

 事件一共包括两部分,一类是新连接事件,一类是有数据可读的事件

问题分析:select函数的readfds是一个传出传入参数

测试和总结select用法

关于select的思考:

 问题: 如果有效的文件描述符比较少,会使循环的次数太多。

 解决办法: 可以将有效的文件描述符放到一个数组当中,这样遍历效率就高了。

select优点:

1.一个进程可以支持多个客户端

2.select支持跨平台

select缺点:

1.代码编写困难

2.会涉及到用户区到内核区的来回拷贝

3.当客户端多个连接,但少数活跃的情况,select效率较低

例如: 作为极端的一种情况, 3-1023文件描述符全部打开,但是只有1023有发送数据,select就显得效率低下

4.最大支持1024个客户端连接

select最大支持1024个客户端连接不是有文件描述符表最多可以支持1024个文件描述符限制的,而是由FD_SETSIZE=1024限制的。

FD_SETSIZE=1024 fd_set使用了该宏,当然可以修改内核,然后再重新编译内核,一般不建议这么做。

作业:

编写代码,让select监控标准输入,监控网络,如果标准输入有数据就写入网络,如果网络有数据就读出网络数据,然后打印到标准输出。

注意: select不仅可以监控socket文件描述符,也可以监视标准输入。

POSIX表示可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准。

关于fd_set类型的底层定义:

/usr/include/x86_64-linux-gnu/sys/select.h和

/usr/include/x86_64-linux-gnu/bits/select.h

在/usr/include/x86_64-linux-gnu/sys/select.h文件中:

__NFDBITS计算出来的值是: 8*8=64

上面是在头文件中一步一步跟踪的定义,最简单的方法就是使用预处理将头文件和宏全部替换掉,直接就可以看到最终的定义了。

如: gcc -E select.c -o select.i

打开select.i后

typedef struct
{
	__fd_mask __fds_bits[1024 / (8 * (int)sizeof(__fd_mask))];

} fd_set;

进一步转换后:

typedef struct
{
	long int __fds_bits[1024 / (8 * 8))];
	//long int __fds_bits[16];	
}

这个数组一共占用: 8 * 16 * 8 = 1024,也就是说fd_set这个文件描述符表中一共有1024个bit位,每个bit位只有0和1两种值,1表示有,0表示没有。

10.poll和epoll(重点是epoll)

学习目标

1.了解poll函数

2.熟练使用epoll多路IO模型

3.了解epoll ET/LT触发模式并实现

4.理解epoll边缘非阻塞模式并实现

5.了解epoll反应堆模型设计思想

6.能看懂epoll反应堆模型的实现代码

10.1多路IO-poll

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

函数说明: 跟select类似,监控多路IO,但poll不能跨平台。

参数说明:

 fds: 传入传出参数,实际上是一个结构体数组 一个struct pollfd结构体数组的首地址

  fds.fd: 要监控的文件描述符

  fds.events:

   POLLIN---->读事件

   POLLOUT---->写事件

  fds.revents: 返回的事件

 nfds: 数组实际有效内容的个数 告诉内核监控的范围,具体是:数组下标的最大值+1

 timeout: 超时时间,单位是毫秒

  -1:永久阻塞,直到监控的事件发生

  0: 不管是否有事件发生,立刻返回

  >0: 直到监控的事件发生或者超时

返回值:

 成功:返回就绪事件的个数

 失败:返回-1

 若timeout=0,poll函数不阻塞,且没有事件发生,此时返回-1,并且errno=EAGAIN,这种情况不应视为错误。

struct pollfd 
{
   int   fd;        /* file descriptor */   要监控的文件描述符
   short events;     /* requested events */  输入参数,表示告诉内核要监控的事件(读事件,写事件,异常事件)---不会被修改
   short revents;    /* returned events */   返回发生变化的事件 ---由内核返回
};

events和revents参数

POLLIN:可读事件

POLL:可写事件

POLLERR:异常事件

说明:

1.当poll函数返回的时候,结构体当中的fd和events没有发生变化,究竟有没有事件发生由revents来判断,所以poll是请求和返回分离。

2.struct pollfd结构体中的fd成员若赋值为-1,则poll不会监控。

3.相对于select,poll没有本质上的改变;但是poll可以突破1024的限制。

在/proc/sys/fs/file-max查看一个进程可以打开的socket描述符上限。

如果需要可以修改配置文件: /etc/security/limits.conf

加入如下配置信息,然后重启终端即可生效。

* soft nofile 1024

* hard nofile 100000

soft和hard分别表示ulimit命令可以修改的最小限制和最大限制

使用poll模型开发服务端流程:

{
1 创建socket,得到监听文件描述符lfd----socket()
2 设置端口复用----setsockopt()
3 绑定----bind()
4 监听----listen()
5 struct pollfd client[1024];
  client[0].fd = lfd;
  client[0].events = POLLIN;
  
  int maxi = 0;
  for(i=1; i<1024; i++)
  {
  	client[i].fd = -1;
  }
  
  while(1)
  {
  	nready = poll(client, maxi+1, -1);
  	//异常情况
  	if(nready<0)
  	{
  		if(errno==EINTR)  // 被信号中断
  		{
  			continue;
  		}
  		break;
  	}
  	
  	//有客户端连接请求到来
  	if(client[0].revents==POLLIN)
  	{
  		//接受新的客户端连接
  		cfd = accept(lfd, NULL, NULL);
  		
  		//寻找在client数组中可用位置
  		for(i=0; i<1024; i++)
  		{
  			if(client[i].fd==-1)
  			{
  				client[i].fd = cfd;
  				client[i].events = POLLIN;
  				break;
  			}
  		}
  		
  		//客户端连接数达到最大值
  		if(i==1024)
  		{
  			close(cfd);
  			continue;
  		}
  		
  		//修改client数组下标最大值
  		if(maxi<i)
  		{
  			maxi = i;
  		}
  		
  		if(--nready==0)
  		{
  			continue;
  		}
  	}
  	
  	//下面是有客户端发送数据的情况
  	for(i=1; i<=maxi; i++)
  	{
  		sockfd = client[i].fd;
  		//如果client数组中fd为-1, 表示已经不再让你内核监控了, 已经close了
  		if(client[i].fd==-1)
  		{
  			continue;
  		}
  		
  		if(client[i].revents==POLLIN)
  		{
  			//read 数据
  			n = read(sockfd, buf, sizeof(buf));
  			if(n<=0)
  			{
  				close(sockfd);
  				client[i].fd = -1;		
  			}
  			else 
  			{
  				//发送数据给客户端
  				write(sockfd, buf, n);
  			} 	
  			
	  		if(--nready==0)
	  		{
	  			break;
	  		}	
  		}  		
  	}  	
  }
 
  close(lfd); 
} 

poll.c

//IO多路复用技术poll函数的使用 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include "wrap.h"

int main()
{
	int i;
	int n;
	int lfd;
	int cfd;
	int ret;
	int nready;
	int maxfd;
	char buf[1024];
	socklen_t len;
	int sockfd;
	fd_set tmpfds, rdfds;
	struct sockaddr_in svraddr, cliaddr;
	
	//创建socket
	lfd = Socket(AF_INET, SOCK_STREAM, 0);

	//允许端口复用
	int opt = 1;
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

	//绑定bind
	svraddr.sin_family = AF_INET;
	svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	svraddr.sin_port = htons(8888);
	ret = Bind(lfd, (struct sockaddr *)&svraddr, sizeof(struct sockaddr_in));

	//监听listen
	ret = Listen(lfd, 128);

	struct pollfd client[1024];
	for(i=0; i<1024; i++)
	{
		client[i].fd = -1;
	}		

	//将监听文件描述符委托给内核监控----监控读事件
	client[0].fd = lfd;
	client[0].events = POLLIN;

	maxfd = 0; //maxfd表示内核监控的范围

	while(1)
	{
		nready = poll(client, maxfd+1, -1);
		if(nready<0)
		{
			perror("poll error");
			exit(1);
		}
		
		//有客户端连接请求
		if(client[0].fd==lfd && (client[0].revents & POLLIN))
		{
			cfd = Accept(lfd, NULL, NULL);

			//寻找client数组中的可用位置
			for(i=1; i<1024; i++)
			{
				if(client[i].fd==-1)
				{
					client[i].fd = cfd;
					client[i].events = POLLIN;
					break;
				}
			}

			//若没有可用位置,则关闭连接
			if(i==1024)
			{
				Close(cfd);
				continue;
			}

			if(maxfd<i)
			{
				maxfd = i;
			}
			
			if(--nready==0)
			{
				continue;
			}
		}

		//下面是有数据到来的情况
		for(i=1; i<=maxfd; i++)
		{
			//若fd为-1, 表示连接已经关闭或者没有连接
			if(client[i].fd==-1)	
			{
				continue;
			}
			
			sockfd = client[i].fd;
			memset(buf, 0x00, sizeof(buf));
			n = Read(sockfd, buf, sizeof(buf));
			if(n<=0)
			{
				printf("read error or client closed,n==[%d]\n", n);
				Close(sockfd);
				client[i].fd = -1; //fd为-1,表示不再让内核监控
			}
			else
			{
				printf("read over,n==[%d],buf==[%s]\n", n, buf);
				write(sockfd, buf, n);
			}

			if(--nready==0)
			{
				break;
			}
		}
	}

	Close(lfd);
	return 0;
}

10.2多路IO-epoll

将检测文件描述符的变化委托给内核去处理,然后内核将发生变化的文件描述符对应的事件返回给应用程序。

函数介绍:

int epoll_create(int size);

函数说明: 创建一个树根

参数说明:

size: 最大节点数,此参数在linux 2.6.8已被忽略,但必须传递一个大于0的数。

返回值:

成功: 返回一个大于0的文件描述符,代表整个树的树根。

失败: 返回-1,并设置errno值。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函数说明: 将要监听的节点在epoll树上添加,删除和修改

参数说明:

epfd: epoll树根

op:

 EPOLL_CTL_ADD: 添加事件节点到树上

 EPOLL_CTL_DEL: 从树上删除事件节点

 EPOLL_CTL_MOD: 修改树上对应的事件节点

fd: 事件节点对应的文件描述符

event: 要操作的事件节点

typedef union epoll_data
{
    void* ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
}epoll_data_t;
struct epoll_event 
{
    uint32_t events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};

event.events常用的有:

  EPOLLIN: 读事件

  EPOLLOUT: 写事件

  EPOLLERR: 错误事件

 EPOLLET: 边缘触发模式

event.data.fd: 要监控的事件对应的文件描述符

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

函数说明:等待内核返回事件发生

参数说明

epfd: epoll树根

events: 传出参数,其实是一个事件结构体数组

maxevents: 数组大小

timeout:

 -1: 表示永久阻塞

 0: 立即返回

 >0: 表示超时等待事件

返回值:

 成功: 返回发生事件的个数

 失败: 若timeout=0,没有事件发生则返回;返回-1,设置errno值

epoll_wait的events是一个传出参数,调用epoll_ctl传递给内核什么值,当epoll_wait返回的时候,内核就传回什么值,不会对struct event的结构体变量的值做任何修改。

使用epoll模型开发服务器流程:

{
1 创建socket,得到监听文件描述符lfd----socket()
2 设置端口复用----setsockopt()
3 绑定----bind()
4 监听----listen()
5 创建一棵epoll树
  int epfd = epoll_create();
	
  //将监听文件描述符上树
  struct epoll_event ev;
  ev.events = EPOLLIN;  //可读事件
  ev.data.fd = lfd;
  epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
  
  struct epoll_event events[1024];
  while(1)
  {
  	nready = epoll_wait(epfd, events, 1024, -1);
  	if(nready<0)
  	{
  		if(errno==EINTR)//被信号中断
  		{
  			continue;
  		}
  		break;
  	}
  	
  	for(i=0; i<nready; i++)
  	{
  		sockfd = events[i].data.fd;
  		//有客户端连接请求到来  	
  		if(sockfd==lfd)
  		{
  			cfd = accept(lfd, NULL, NULL);
  			
  			//将cfd对应的读事件上epoll树
  			ev.data.fd = cfd;
  			ev.events = EPOLLIN; 
  			epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
  			continue;
  		}
  		
  		//有客户端发送数据过来
  		n = Read(sockfd, buf, sizeof(buf));
  		if(n<=0)
  		{
  			close(sockfd);
  			//将sockfd对应的事件节点从epoll树上删除
  			epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
  			perror("read error or client closed");
  			continue;
  		}
  		else 
  		{
  			write(sockfd, buf, n);
  		}
  	} 
  }
  
  Close(epfd);
  close(lfd);
  
  return 0;
}

01-epoll.c

//EPOLL模型测试
#include "wrap.h"
#include <sys/epoll.h>
#include <ctype.h>

int main()
{
	int ret;
	int n;
	int i;
	int k;
	int nready;
	int lfd;
	int cfd;
	int sockfd;
	char buf[1024];
	socklen_t socklen;
	struct sockaddr_in svraddr;
	struct epoll_event ev;
	struct epoll_event events[1024];
	
	//创建socket
	lfd = Socket(AF_INET, SOCK_STREAM, 0);
	
	//设置文件描述符为端口复用
	int opt = 1;
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

	//绑定bind
	svraddr.sin_family = AF_INET;
	svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	svraddr.sin_port = htons(8888);
	Bind(lfd, (struct sockaddr *)&svraddr, sizeof(struct sockaddr_in));
	
	//监听listen
	Listen(lfd, 128);
	
	//创建一棵epoll树
	int epfd = epoll_create(1024);
	if(epfd<0)
	{
		perror("create epoll error");
		return -1;
	}
	
	//将lfd上epoll树
	ev.data.fd = lfd;
	ev.events = EPOLLIN;
	epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
	
	while(1)
	{
		nready = epoll_wait(epfd, events, 1024, -1);
		if(nready<0)
		{
			perror("epoll_wait error");
			if(errno==EINTR)
			{
				continue;
			}
			break;			
		}
		
		for(i=0; i<nready; i++)
		{
			//有客户端连接请求
			sockfd = events[i].data.fd;
			if(sockfd==lfd)
			{
				cfd = Accept(lfd, NULL, NULL);
				//将新的cfd上epoll树
				ev.data.fd = cfd;
				ev.events = EPOLLIN;
				epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
				continue;
			}
			
			//有客户端发送数据过来
			memset(buf, 0x00, sizeof(buf));
			//n = Read(sockfd, buf, sizeof(buf));
			n = recv(sockfd, buf, sizeof(buf), 0);
			if(n<=0)
			{
				printf("n==[%d], buf==[%s]\n", n, buf);
				close(sockfd);
				//将sockfd对应的事件就节点从epoll树上删除
				epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
				
			}
			else 
			{
				printf("n==[%d], buf==[%s]\n", n, buf);
				for(k=0; k<n; k++)
				{
					buf[k] = toupper(buf[k]);
				}
				//Write(sockfd, buf, n);
				send(sockfd, buf, n, 0);
			}
		}
	}
	
	close(epfd);
	close(lfd);
	return 0;
}

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ gcc -o epoll 01-epoll.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ ./epoll
n==[6], buf==[nihao
]

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ nc 127.1 8888
nihao
NIHAO

终端3

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ netstat -anp | grep 8888
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN      20319/./epoll       
tcp        0      0 127.0.0.1:8888          127.0.0.1:44728         ESTABLISHED 20319/./epoll       
tcp        0      0 127.0.0.1:44728         127.0.0.1:8888          ESTABLISHED 20320/nc            

10.3进阶epoll

1.介绍epoll的两种工作模式

epoll的两种模式ET和LT模式

 水平触发: 高电平代表1

  只要缓冲区中有数据,就一直通知

 边缘触发: 电平有变化就代表1

  缓冲区中有数据只会通知一次,之后再有数据才会通知。(若是读数据的时候没有读完,则剩余的数据不会再通知,直到有新的数据到来)

边缘非阻塞模式:提高效率

2.用实验验证LT和ET模式

ET模式由于只通知一次,所以在读的时候要循环读,直到读完,但是当读完之后read就会阻塞,所以应该将该文件描述符设置为非阻塞模式(fcntl函数)。

read函数在非阻塞模式下读的时候,若返回-1,且errno为EAGAIN,则表示当前资源不可用,也就是说缓冲区无数据(缓冲区的数据已经读完了);或者当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲区中已没有数据可读了,也就可以认为此时读事件已处理完成。

//EPOLL模型测试:测试ET和LT模式的区别
#include "wrap.h"
#include <sys/epoll.h>
#include <ctype.h>

int main()
{
	int ret;
	int n;
	int i;
	int k;
	int nready;
	int lfd;
	int cfd;
	int sockfd;
	char buf[1024];
	socklen_t socklen;
	struct sockaddr_in svraddr;
	struct epoll_event ev;
	struct epoll_event events[1024];
	
	//创建socket
	lfd = Socket(AF_INET, SOCK_STREAM, 0);
	
	//设置文件描述符为端口复用
	int opt = 1;
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

	//绑定bind
	svraddr.sin_family = AF_INET;
	svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	svraddr.sin_port = htons(8888);
	Bind(lfd, (struct sockaddr *)&svraddr, sizeof(struct sockaddr_in));
	
	//监听listen
	Listen(lfd, 128);
	
	//创建一棵epoll树
	int epfd = epoll_create(1024);
	if(epfd<0)
	{
		perror("create epoll error");
		return -1;
	}
	
	//将lfd上epoll树
	ev.data.fd = lfd;
	ev.events = EPOLLIN;
	epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
	
	while(1)
	{
		nready = epoll_wait(epfd, events, 1024, -1);
		if(nready<0)
		{
			perror("epoll_wait error");
			if(errno==EINTR)
			{
				continue;
			}
			break;			
		}
		
		for(i=0; i<nready; i++)
		{
			//有客户端连接请求
			sockfd = events[i].data.fd;
			if(sockfd==lfd)
			{
				cfd = Accept(lfd, NULL, NULL);
				//将新的cfd上epoll树
				ev.data.fd = cfd;
				ev.events = EPOLLIN;//ev.events = EPOLLIN | EPOLLET
                ;
				epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
				continue;
			}
			
			//有客户端发送数据过来
			memset(buf, 0x00, sizeof(buf));
			//n = Read(sockfd, buf, sizeof(buf));
			n = recv(sockfd, buf, 2, 0);
			if(n<=0)
			{
				printf("n==[%d], buf==[%s]\n", n, buf);
				close(sockfd);
				//将sockfd对应的事件就节点从epoll树上删除
				epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
			}
			else 
			{
				printf("n==[%d], buf==[%s]\n", n, buf);
				for(k=0; k<n; k++)
				{
					buf[k] = toupper(buf[k]);
				}
				//Write(sockfd, buf, n);
				send(sockfd, buf, n, 0);
			}
		}
	}
	
	close(epfd);
	close(lfd);
	return 0;
}

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ gcc -o epoll_lt 02-epoll.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ ./epoll_lt
n==[2], buf==[11]
n==[2], buf==[22]
n==[2], buf==[33]
n==[2], buf==[44]
n==[2], buf==[55]
n==[1], buf==[
]

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ nc 127.1 8888
1122334455
1122334455


这段代码是一个使用 epoll 实现的服务器程序,主要用于测试 EPOLL 模型中的 ET(边缘触发)和 LT(水平触发)模式的区别。

以下是代码主要的逻辑:

  1. 创建并绑定 socket
    • 创建一个监听 socket,并设置为端口复用。
    • 绑定监听地址和端口。
    • 开始监听连接请求。
lfd = Socket(AF_INET, SOCK_STREAM, 0);

//设置文件描述符为端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

//绑定bind
svraddr.sin_family = AF_INET;
svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
svraddr.sin_port = htons(8888);
Bind(lfd, (struct sockaddr *)&svraddr, sizeof(struct sockaddr_in));

//监听listen
Listen(lfd, 128);
  1. 创建 epoll 树,并将监听 socket 加入 epoll 树中。
//创建一棵epoll树
int epfd = epoll_create(1024);
if(epfd < 0)
{
    perror("create epoll error");
    return -1;
}

//将lfd上epoll树
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
  1. 进入主循环,调用 epoll_wait 等待事件的发生。
while(1)
{
    nready = epoll_wait(epfd, events, 1024, -1);
    if(nready < 0)
    {
        perror("epoll_wait error");
        if(errno == EINTR)
        {
            continue;
        }
        break;
    }
  1. 遍历就绪事件,并根据事件类型进行处理。
  • 如果是监听 socket 上有连接请求,接受新的连接,并将新连接的 socket 也加入 epoll 树中。
//有客户端连接请求
sockfd = events[i].data.fd;
if(sockfd == lfd)
{
    cfd = Accept(lfd, NULL, NULL);
    //将新的cfd上epoll树
    ev.data.fd = cfd;
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
    continue;
}
  • 如果是已连接 socket 上有数据可读,读取数据并将数据转换为大写形式发送回客户端。
//有客户端发送数据过来
memset(buf, 0x00, sizeof(buf));
//n = Read(sockfd, buf, sizeof(buf));
n = recv(sockfd, buf, 2, 0);
if(n <= 0)
{
    printf("n==[%d], buf==[%s]\n", n, buf);
    close(sockfd);
    //将sockfd对应的事件就节点从epoll树上删除
    epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
}
else 
{
    printf("n==[%d], buf==[%s]\n", n, buf);
    for(k=0; k<n; k++)
    {
        buf[k] = toupper(buf[k]);
    }
    //Write(sockfd, buf, n);
    send(sockfd, buf, n, 0);
}
  1. 最后关闭 epoll 树和监听 socket
close(epfd);
close(lfd);

总体而言,这段代码主要是一个简单的使用 epoll 的服务器程序,用于测试 ETLT 模式在事件触发上的不同。在这里,主要关注了监听 socket 和已连接 socket 上的读事件。需要注意的是,这里采用了非阻塞方式处理套接字。

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ gcc -o epoll_et 02-epoll.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ ./epoll_et
n==[2], buf==[11]

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ nc 127.1 8888
1122334455
11

回车继续发数据

终端1

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ gcc -o epoll_et 02-epoll.c wrap.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ ./epoll_et
n==[2], buf==[11]
n==[2], buf==[22]
n==[2], buf==[33]

终端2

cmt@cmt-VMware-Virtual-Platform:~/C_Program/day11$ nc 127.1 8888
1122334455
11
22
33

epoll的LT和ET模式:
1.epoll默认情况下是LT模式,在这种模式下,若读数据一次性没有读完,缓冲区中还有可读数据, 则epoll_wait还会再次通知
2.若将epoll设置为ET模式,若读数据的时候一次性没有读完,则epoll_wait不再通知,直到下次有新的数据发来。

10.4epoll反应堆

反应堆: 一个小事件触发一系列反应。

epoll反应堆的思想:C++的封装思想(把数据和操作封装到一起)

 --将描述符,事件,对应的处理方法封装在一起

 --当描述符对应的事件发生了,自动调用处理方法(其实原理就是回调函数)

typedef union epoll_data
{
	void* ptr;
	int          fd;
	uint32_t     u32;
	uint64_t     u64;
} epoll_data_t;

struct epoll_event 
{
	uint32_t     events;      /* Epoll events */
	epoll_data_t data;        /* User data variable */
};

epoll反应堆的核心思想是: 在调用epoll_ctl函数的时候, 将events上树的时候,利用epoll_data_t的ptr成员, 将一个文件描述符,事件和回调函数封装成一个结构体,然后让ptr指向这个结构体,然后调用epoll_wait函数返回的时候,可以得到具体的events,然后获得events结构体中的events.data.ptr指针,ptr指针指向的结构体中有回调函数,最终可以调用这个回调函数。

posted @   CodeMagicianT  阅读(56)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示