网络IO模型

网络五元组

sip(源IP地址),dip(目标IP地址),sport(源端口号),dport(目标端口号),protocol(传输层协议,即TCP/UDP)就是网络五元组,每个socket由五元组组成,通过五元组之间不同的组合可以产生不同的socket

socket是什么?

socket是文件描述符,也可以说是一个句柄,bind(),connect(),send(),recv(),close()等函数可以通过这个句柄来找到对应socket这个结构体进行操作
在linux内核的include/net.h文件中可以看出socket的一些属性

struct socket {
	socket_state		state; 		//socket的状态
	short			type;			//套接字类型,如SOCK_STREAM和SOCK_DGRAM
	unsigned long		flags;
	struct file		*file;
	struct sock		*sk;
	const struct proto_ops	*ops;   //socket的操作
	struct socket_wq	wq;
};

//socket的操作方法
struct proto_ops {					
	int		family;
	struct module	*owner;
	int		(*release)   (struct socket *sock);
	int		(*bind)	     (struct socket *sock,
				      struct sockaddr *myaddr,
				      int sockaddr_len);
	int		(*connect)   (struct socket *sock,
				      struct sockaddr *vaddr,
				      int sockaddr_len, int flags);
	int		(*socketpair)(struct socket *sock1,
				      struct socket *sock2);
	int		(*accept)    (struct socket *sock,
				      struct socket *newsock, int flags, bool kern);
	int		(*getname)   (struct socket *sock,
				      struct sockaddr *addr,
				      int peer);
	__poll_t	(*poll)	     (struct file *file, struct socket *sock,
				      struct poll_table_struct *wait);
	int		(*ioctl)     (struct socket *sock, unsigned int cmd,
				      unsigned long arg);
	int		(*gettstamp) (struct socket *sock, void __user *userstamp,
				      bool timeval, bool time32);
	int		(*listen)    (struct socket *sock, int len);
.........................
};

五种 IO 网络模型

recv()和read()一般为阻塞,如果读缓存区的数据为空,就会阻塞。
send()和write()一般也是阻塞,如果写入的数据超过了写缓冲区剩余空间(即没有足够的空间放入数据),就会阻塞

  • recv() 返回值大于 0,表示接受数据完毕,返回值即是接受到的字节数;
  • recv() 返回 0,表示连接已经正常断开;
  • recv() 返回 -1,且 errno 等于 EAGAIN,表示 recv 操作还没执行完成;
  • recv() 返回 -1,且 errno 不等于 EAGAIN,表示 recv 操作遇到系统错误 errno。

1.阻塞IO(blocking IO)

在linux中,所有socket一般都是阻塞(blocking)的,一个典型的读操作流程
在这里插入图片描述

用户进程通过read()调用内核socket的read()接口,内核就开始第一阶段:等待数据(等待接收完整的数据包),用户进程会一直阻塞,当内核的数据准备好了,开始第二阶段:拷贝数据(把数据包拷贝到用户空间),直到这两个阶段完成,read()才解除阻塞(blocking)状态

缺点:每次数据没有就绪时,用户进程需要一直等待。

TCP和UDP服务端代码(需要指定端口)

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

#define TCP 1

//线程函数
void* client_callback(void *arg) {
	
	int clientfd = *(int*)arg;
	char buf[256] = {0};
	int len = 0;
	
	while (1) {
		
            len = recv(clientfd, buf, 256, 0);
			//char str[20] = {0};
			//获取客户端IP地址
            //inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str));
            if (len > 0)
                printf("From %d msg : %s\n", clientfd, buf);
            //len = send(clientfd, buffer, len, 0);
	}
}

int main(int argc, char *argv[]) {

		if (argc != 2) {
			printf("Usage:\n");
	        printf("%s <port>\n", argv[0]);
	        return -1;
		}
#if TCP
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        socklen_t client_len = sizeof(struct sockaddr);
#else
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        int client_len = sizeof(struct sockaddr);
#endif

        struct sockaddr_in serv_addr;
        memset(&serv_addr, 0, sizeof(serv_addr));

        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(atoi(argv[1]));
        serv_addr.sin_addr.s_addr = INADDR_ANY;

        struct sockaddr_in client_addr;
        memset(&client_addr, 0, sizeof(client_addr));

        bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
#if TCP
		//第二个参数是最大连接数
        if (listen(sockfd, 5) < 0) {
                perror("listen");
                return -1;
        }
#endif	
	
        while(1) {
#if TCP
			int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
			pthread_t thread_id;
			int ret = pthread_create(&thread_id, NULL, client_callback, &clientfd);
#else
            char buf[256] = {0};
            int len = recvfrom(sockfd, buf, 256, 0, (struct sockaddr *)&client_addr, &client_len);
            char str[20] = {0};
            //获取客户端IP地址
            inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str));
            if (len > 0)
                    printf("From %s msg : %s\n", str, buf);
            //len = sendto(sockfd, buffer, len, 0, (struct sockaddr*)&client_addr, len);
#endif
        }
        close(sockfd);

        return 0;
}

TCP和UDP客户端代码(需要指定端口和IP地址)

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

#include <string.h>
#include <unistd.h>

#define TCP 1

int main(int argc, char **argv) {

        if (argc != 3)
        {
                printf("Usage:\n");
                printf("%s <port> <server_ip>\n", argv[0]);
                return -1;
        }
#if TCP
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
#else
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        socklen_t server_len = sizeof(struct sockaddr);
#endif

        struct sockaddr_in server_addr;
        memset(&server_addr, 0, sizeof(server_addr));

        server_addr.sin_family = AF_INET;
        //先将字符串转数字,再转网络字节序
        server_addr.sin_port = htons(atoi(argv[1]));
        server_addr.sin_addr.s_addr = inet_addr(argv[2]);

        bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
#if TCP
        //udp连接调用connect建立连接,1.可以提高效率,减少重复建立连接的过程;2.高并发中增加系统稳定性
        int ret = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
        if(ret < 0) {
            perror("connect");
            return -1;
        }
#endif

        int len = 0;
        char buf[256] = {0};

        while(1) {
        
            fgets(buf, 256, stdin);
#if TCP
            len = send(sockfd, buf, 256, 0);
            if (len > 0)
                printf("send %s msg : %s\n", argv[1], buf);
            //len = recv(clientfd, buffer, len, 0);
#else
            len = sendto(sockfd, buf, 256, 0, (struct sockaddr *)&server_addr, server_len);
            if (len > 0)
                    printf("send %s msg : %s\n", argv[1], buf);
            //len = recvfrom(sockfd, buffer, len, 0, (struct sockaddr*)&server_addr, server_len);
#endif
        }
        close(sockfd);

        return 0;
}

2.非阻塞IO(non-blocking IO)

在linux中,可以通过设置fcntl()设置socket为非阻塞(non-blocking IO),当进行读操作时,流程是
在这里插入图片描述

用户进程通过read()调用内核socket的read()接口,如果内核的数据没有准备好,在非阻塞的情况下,内核没有进入第一阶段:等待数据(等待接收完整的数据包)就直接返回了,当内核的数据准备好了,开始第二阶段:拷贝数据(把数据包拷贝到用户空间)
缺点:用户进程会不断地询问内核数据是否就绪,而一直占用CPU。
在原来服务端的基础上通过fcntl()设置socket属性

//F_GETFL获取socket的flags
int flags = fcntl(sockfd, F_GETFL, 0);
//flags加上O_NONBLOCK
flags |= O_NONBLOCK;
//F_SETFL设置flags
fcntl(sockfd, F_SETFL, flags);

TCP/UDP服务端代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>

#define TCP 1

//线程函数
void* client_callback(void *arg) {
	
	int clientfd = *(int*)arg;
	char buf[256] = {0};
	int len = 0;
	//F_GETFL获取socket的flags
	int flags = fcntl(clientfd, F_GETFL, 0);
	//flags加上O_NONBLOCK
	flags |= O_NONBLOCK;
	//F_SETFL设置flags
	fcntl(clientfd, F_SETFL, flags);
	while (1) {
		
            len = recv(clientfd, buf, 256, 0);
			//char str[20] = {0};
			//获取客户端IP地址
            //inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str));
            if (len > 0)
                printf("From %d msg : %s\n", clientfd, buf);
			else
				printf("not msg\n");
            //len = send(clientfd, buffer, len, 0);
	}
}

int main(int argc, char *argv[]) {

		if (argc != 2) {
			printf("Usage:\n");
	        printf("%s <port>\n", argv[0]);
	        return -1;
		}
#if TCP
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        socklen_t client_len = sizeof(struct sockaddr);
#else
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        int client_len = sizeof(struct sockaddr);
#endif

        struct sockaddr_in serv_addr;
        memset(&serv_addr, 0, sizeof(serv_addr));

        serv_addr.sin_family = AF_INET;
		//先将字符串转数字,再转网络字节序
        serv_addr.sin_port = htons(atoi(argv[1]));
        serv_addr.sin_addr.s_addr = INADDR_ANY;

        struct sockaddr_in client_addr;
        memset(&client_addr, 0, sizeof(client_addr));

        bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
#if TCP
		//第二个参数是最大连接数
        if (listen(sockfd, 5) < 0) {
                perror("listen");
                return -1;
        }
#else
		//F_GETFL获取socket的flags
		int flags = fcntl(sockfd, F_GETFL, 0);
		//flags加上O_NONBLOCK
		flags |= O_NONBLOCK;
		//F_SETFL设置flags
		fcntl(sockfd, F_SETFL, flags);		
#endif	
	
        while(1) {
#if TCP
			int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
			pthread_t thread_id;
			int ret = pthread_create(&thread_id, NULL, client_callback, &clientfd);
#else
            char buf[256] = {0};
            int len = recvfrom(sockfd, buf, 256, 0, (struct sockaddr *)&client_addr, &client_len);
            char str[20] = {0};
            //获取客户端IP地址
            inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str));
            if (len > 0)
                    printf("From %s msg : %s\n", str, buf);
			//else
				//printf("not msg\n");		
            //len = sendto(sockfd, buffer, len, 0, (struct sockaddr*)&client_addr, len);
#endif
        }
        close(sockfd);

        return 0;
}

3.异步 IO(Asynchronous I/O)

Linux 下的异步IO( asynchronous IO )用在磁盘 IO 读写操作,不用于网络 IO,从内核 2.6 版本才开始引入。流程如下
在这里插入图片描述

用户进程调用aio_read()后,就把等待数据(等待接收完整的数据包)和拷贝数据(把数据包拷贝到用户空间)的过程丢给内核,到数据拷贝完成后在进行处理
优点:在内核等待数据和拷贝数据的两个阶段,用户进程都不是阻塞的
缺点:开发复杂,在高并发情况下容易任务堆积
代码暂无

4.信号驱动 IO(signal driven I/O, SIGIO)

在linux中,可以通过设置sigaction()让内核捕捉SIGIO信号时调用回调函数,流程是
在这里插入图片描述

当有IO事件后,就会产生SIGIO信号调用回调函数,相当于有数据就绪才调用read()
通过signal()或sigaction()函数设置信号触发函数

//Unix的函数,调用一次函数后失效
sign *signal(int, handler *);
//在Linux,signal函数已被改写,由sigaction函数封装实现
int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);

设置接收信号和回调函数

struct sigaction sigio_action;
//信号选项标志
sigio_action.sa_flags = 0;
//设置回调函数
sigio_action.sa_handler = do_sigio;
//设置SIGIO为触发信号
sigaction(SIGIO, &sigio_action, NULL);

设置socket为非阻塞和异步IO,
异步IO:把socket丢给内核处理

//getpid()获取当前进程id,
//F_SETOWN参数可以设置该进程id可以接受SIGIO和SIGURG信号
fcntl(sockfd, F_SETOWN, getpid());
//F_GETFL获取socket的flags
int flags = fcntl(sockfd, F_GETFL, 0);
//flags加上O_ASYNC和O_NONBLOCK
//O_ASYNC:设置socket为异步IO,允许SIGIO信号发送到进程
//O_NONBLOCK:设置socket成非阻塞
flags |= O_ASYNC |O_NONBLOCK;
//F_SETFL设置flags
fcntl(sockfd, F_SETFL, flags);

优点:等到有数据才会开始接收数据,进程影响小
缺点:这个一般用于UDP中,对TCP几乎是没用的,因为TCP信号过于频繁

UDP服务端代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>

struct sockaddr_in client_addr;
int sockfd;

//回调函数
void do_sigio(int sig) {

	int client_len = sizeof(struct sockaddr_in);

	char buffer[256] = {0};
	int len = recvfrom(sockfd, buffer, 256, 0, (struct sockaddr*)&client_addr, (socklen_t*)&client_len);
	printf("Listen Message : %s\r\n", buffer);

	//int slen = sendto(sockfd, buffer, len, 0, (struct sockaddr*)&client_addr, client_len);
}

int main(int argc, char **argv) {

		if (argc != 2)
        {
                printf("Usage:\n");
                printf("%s <port>\n", argv[0]);
                return -1;
        }

        sockfd = socket(AF_INET, SOCK_DGRAM, 0);
		
		struct sigaction sigio_action;
		sigio_action.sa_flags = 0;
		//设置回调函数
		sigio_action.sa_handler = do_sigio;
		//设置SIGIO为触发信号
		sigaction(SIGIO, &sigio_action, NULL);

        struct sockaddr_in serv_addr;
        memset(&serv_addr, 0, sizeof(serv_addr));

        serv_addr.sin_family = AF_INET;
        //先将字符串转数字,再转网络字节序
        serv_addr.sin_port = htons(atoi(argv[1]));
        serv_addr.sin_addr.s_addr = INADDR_ANY;

		//getpid()获取当前进程id,
		//F_SETOWN参数可以设置该进程id可以接受SIGIO和SIGURG信号
		fcntl(sockfd, F_SETOWN, getpid());
        //F_GETFL获取socket的flag
        int flags = fcntl(sockfd, F_GETFL, 0);
        //flag加上O_ASYNC和O_NONBLOCK
		//O_ASYNC:允许SIGIO信号发送到进程
		//O_NONBLOCK:设置socket成非阻塞
        flags |= O_ASYNC |O_NONBLOCK;
        //F_SETFL设置flags
        fcntl(sockfd, F_SETFL, flags);

        memset(&client_addr, 0, sizeof(client_addr));

        bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

        while(1)
			sleep(1);
				
        close(sockfd);
        
        return 0;
}

1.信号如何保存到进程里面?

当我们调用sigaction()函数后,内核会调用kernel/signal.c的SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler)函数,再通过do_sigaction()函数可以看出任务进程的信号保存在struct k_sigaction action[]这个结构体数组里

SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler)
{
	...
	ret = do_sigaction(sig, &new_sa, &old_sa);
	...
}

int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
{
	struct task_struct *p = current;	//当前的任务进程
	struct k_sigaction *k;				//保存信号
	...
	k = &p->sighand->action[sig-1];		//任务进程的信号
	...
	if (oact)
		*oact = *k;
	...
}

2.进程的信号集合如何保存在哪里?

action数组定义在linux内核的include/linux/sched.h的task_struct->sighand.action[]中,
action[].sa->_sa_sigaction就是保存信号处理函数的

include/linux/sched.h
struct task_struct {
	...
	struct sighand_struct		*sighand;
	...
}
include/linux/sched/signal.h
struct sighand_struct {
	spinlock_t		siglock;
	refcount_t		count;
	wait_queue_head_t	signalfd_wqh;
	struct k_sigaction	action[_NSIG];	//存储信号集合,_NSIG = 64
};
include/linux/signal_types.h
struct k_sigaction {
	struct sigaction sa;				
#ifdef __ARCH_HAS_KA_RESTORER
	__sigrestore_t ka_restorer;
#endif
}
/arch/x86/include/uapi/asm/signal.h
struct sigaction {
	//这个一个联合体,通过句柄和回调函数处理二选一
	union {														
	  __sighandler_t _sa_handler;								//信号的处理句柄
	  void (*_sa_sigaction)(int, struct siginfo *, void *);		//回调函数
	} _u;
	sigset_t sa_mask;											//执行时的屏蔽号
	unsigned long sa_flags;	
	void (*sa_restorer)(void);
};

信号集合保存在action[_NSIG]里,正好有64个

 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

3.信号如何发送

当我们调用kill()函数后,内核会调用kernel/signal.c的SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)函数,再通过一系列函数:kill_something_info()->kill_pid_info()->group_send_sig_info()->do_send_sig_info()->send_signal()->__send_signal()->signalfd_notify()->wake_up()唤醒进程的信号队列

SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
	...
	return kill_something_info(sig, &info, pid);
}
static int kill_something_info(int sig, struct kernel_siginfo *info, pid_t pid)
{
	...
	ret = kill_pid_info(sig, info, find_vpid(pid));
	...
}
int kill_pid_info(int sig, struct kernel_siginfo *info, struct pid *pid)
{
	...
	group_send_sig_info(sig, info, p, PIDTYPE_TGID);
	...
}
int group_send_sig_info(int sig, struct kernel_siginfo *info,
			struct task_struct *p, enum pid_type type)
{
	...
	ret = do_send_sig_info(sig, info, p, type);
	...
}
int do_send_sig_info(int sig, struct kernel_siginfo *info, struct task_struct *p,
			enum pid_type type)
{
	...
	ret = send_signal(sig, info, p, type);
	...
}
static int send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
			enum pid_type type)
{
	...
	return __send_signal(sig, info, t, type, force);
}
static int __send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
			enum pid_type type, bool force)
{
	...
	signalfd_notify(t, sig);
	...
}
static inline void signalfd_notify(struct task_struct *tsk, int sig)
{
	if (unlikely(waitqueue_active(&tsk->sighand->signalfd_wqh)))
		wake_up(&tsk->sighand->signalfd_wqh);
}

5.多路复用 IO (IO multiplexing )

多路复用的意思就是单个进程就可以同时处理多个网络连接的 IO,原理就是通过 select/poll/epoll不断的轮询所负责的所有socket,当某个socket有数据到达就是通知用户进程,流程如下
在这里插入图片描述

当用户进程调用select,整个进程也会被block,同时把等待数据(等待接收完整的数据包)的过程丢给select,当内核的数据准备好了,select就会通知read进行拷贝数据(把数据包拷贝到用户空间)的操作,实际上select/poll/epoll并不比阻塞IO性能好,可能延迟会更大,但是另有用处
多路复用一般用于多进程/多线程中,优点在于能够不断地处理更多的连接,而不是对于连接处理的快慢
多路复用使用的方式有select(),poll(),epoll(),其中select和poll为POSIX标准中的,而epoll是Linux特有的

select

select把需要的socket都绑定到一个数组里,然后不断的轮询监听socket是否有事件产生,在连接数低的情况下,select效率比poll和epoll高,当连接数很多时,效率很低
主要结构体

#define __FD_SETSIZE	1024
typedef __kernel_fd_set		fd_set;
typedef struct {
	unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

可以看出这个结构体存放的是文件(socket)句柄的集合,上限是1024个,要想监听更多的文件就要修改内核了,但是也不建议,因为我们有poll和epoll
select的接口如下:

FD_ZERO(fd_set* fds)						//清理句柄集合
FD_SET(int fd, fd_set* fds)					//句柄集合对应fd的位置被置为1
FD_ISSET(int fd, fd_set* fds)				//检测句柄集合对应fd的位置是否仍然为1
FD_CLR(int fd, fd_set* fds)					//句柄集合对应fd的位置被清0
//nfds是设置select需要轮询多少个fd,最多设置1024
//readfds是读事件,writefds是写事件,exceptfds是异常事件,当socket有数据产生对应事件就会置1
//timeout为NULL,会一直阻塞监听事件,设置0不管有否有事件都会返回,大于0设置等待时间
//select的返回值<0select错误,0等待超时,>0有事件产生
int select(int nfds, fd_set *readfds, fd_set *writefds,
 fd_set *exceptfds, struct timeval *timeout)
 struct timeval {
	__kernel_time_t		tv_sec;		/* seconds */
	__kernel_suseconds_t	tv_usec;	/* microseconds */
};

select服务端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <errno.h>

#define BUFFER_LENGTH	1024

int main(int argc, char *argv[]) {

	if (argc < 2) {
		printf("Paramter Error\n");
		return -1;
	}
	int port = atoi(argv[1]);

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd < 0) {
		perror("socket");
		return -1;
	}

	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(struct sockaddr_in));

	addr.sin_family = AF_INET;
	addr.sin_port = htons(atoi(argv[1]));
	addr.sin_addr.s_addr = INADDR_ANY;

	if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
		perror("bind");
		return 2;
	}

	if (listen(sockfd, 5) < 0) {
		perror("listen");
		return 3;
	}

	fd_set rfds, rset;

	FD_ZERO(&rfds);	
	//为服务端socket设置读事件		
	FD_SET(sockfd, &rfds);

	int max_fd = sockfd;	//从第一个socket开始监听
	int i = 0;

	while (1) {
		//把上次客户端的读事件也加到select上
		rset = rfds;

		//监听socket+1句柄,只设置了读事件
		int nready = select(max_fd+1, &rset, NULL, NULL, NULL);
		if (nready < 0) {
				printf("select error : %d\n", errno);
				continue;
        }

		//等待accept的事件,因为accept()也算是读事件
        if (FD_ISSET(sockfd, &rset)) {
		struct sockaddr_in client_addr;
		memset(&client_addr, 0, sizeof(struct sockaddr_in));
		socklen_t client_len = sizeof(client_addr);
	
		int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
		if (clientfd <= 0) continue;

		char str[INET_ADDRSTRLEN] = {0};
		printf("recvived from %s at port %d, sockfd:%d, clientfd:%d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
			ntohs(client_addr.sin_port), sockfd, clientfd);

		if (max_fd == FD_SETSIZE) {
			printf("clientfd --> out range\n");
			break;
		}

		//为客户端fd设置读事件
		FD_SET(clientfd, &rfds);

		if (clientfd > max_fd) max_fd = clientfd;

		//nready不等于0代表就accept之后还有读事件
		if (--nready == 0) continue;
	}

		//sockfd + 1之后的是客户端的fd
		for (i = sockfd + 1;i <= max_fd;i ++) {
			//监听所有客户端的读事件
			if (FD_ISSET(i, &rset)) {
				char buffer[BUFFER_LENGTH] = {0};
								printf("Recv,sockfd %d\n", i);
				int ret = recv(i, buffer, BUFFER_LENGTH, 0);

				if (ret < 0) {
					if (errno == EAGAIN || errno == EWOULDBLOCK) {
						printf("read all data");
					}
					//清除客户端的事件
					FD_CLR(i, &rfds);
					close(i);
				} else if (ret == 0) {
					printf(" disconnect %d\n", i);
					//ret == 0是客户端断开连接,清除客户端的事件
					FD_CLR(i, &rfds);
					close(i);
					break;
				} else {
					printf("Recv: %s,sockfd %d\n", buffer, i);
				}
				//没有读事件就退出
				if (--nready == 0) break;
			}
		}	
	}
}

poll

poll可以自己设置存储句柄的数组的大小,从select的三个句柄集合改为一个状态字,但轮询的问题还是没有解决,但我们还有epoll
主要结构体

struct pollfd {
	int fd;					//句柄
	short events;			//等待事件
	short revents;			//返回的事件
};

poll的接口

//fd[]是socket的集合,nfds是socket的数量,
//timeout为-1,会一直阻塞监听事件,设置0不管有否有事件都会返回,大于0设置等待时间
//poll返回值>0状态的总数量,等于0超时,-1调用失败
int poll(struct pollfd fd[], nfds_t nfds, int timeout);

poll服务端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/poll.h>

#define BUFFER_LENGTH	1024
#define POLL_SIZE		1024

int main(int argc, char *argv[]) {

	if (argc < 2) {
		printf("Paramter Error\n");
		return -1;
	}
	int port = atoi(argv[1]);

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd < 0) {
		perror("socket");
		return -1;
	}

	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(struct sockaddr_in));

	addr.sin_family = AF_INET;
	addr.sin_port = htons(atoi(argv[1]));
	addr.sin_addr.s_addr = INADDR_ANY;

	if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
		perror("bind");
		return 2;
	}

	if (listen(sockfd, 5) < 0) {
		perror("listen");
		return 3;
	}

	struct pollfd fds[POLL_SIZE] = {0};
	//设置第一个fd是服务端的fd
	fds[0].fd = sockfd;
	//等待事件是POLLIN
	fds[0].events = POLLIN;

	int max_fd = 0, i = 0;
	for (i = 1;i < POLL_SIZE;i ++) {
		fds[i].fd = -1;
	}

	while (1) {
		//监听所有socket,数量是上一次 + 1
		int nready = poll(fds, max_fd+1, 5);
		if (nready <= 0) continue;

		//第0个fd是服务端的,事件是POLLIN,代表有客户端进行连接
		if ((fds[0].revents & POLLIN) == POLLIN) {
			
			struct sockaddr_in client_addr;
			memset(&client_addr, 0, sizeof(struct sockaddr_in));
			socklen_t client_len = sizeof(client_addr);
		
			int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
			if (clientfd <= 0) continue;

			char str[INET_ADDRSTRLEN] = {0};
			printf("recvived from %s at port %d, sockfd:%d, clientfd:%d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
				ntohs(client_addr.sin_port), sockfd, clientfd);

			//设置客户端的fd
			fds[clientfd].fd = clientfd;
			//等待事件是POLLIN
			fds[clientfd].events = POLLIN;

			if (clientfd > max_fd) max_fd = clientfd;

			//nready不等于0,还有连接事件
			if (--nready == 0) continue;
		}

		//sockfd之后的都是客户端的fd,遍历每个客户端,询问是否有POLLIN事件
		for (i = sockfd + 1;i <= max_fd;i ++) {
			if (fds[i].revents & (POLLIN|POLLERR)) {
				char buffer[BUFFER_LENGTH] = {0};
				int ret = recv(i, buffer, BUFFER_LENGTH, 0);
				if (ret < 0) {
					if (errno == EAGAIN || errno == EWOULDBLOCK) {
						printf("read all data");
					}
					
					//close(i);
					fds[i].fd = -1;
				} else if (ret == 0) {
					printf(" disconnect %d\n", i);
					
					close(i);
					fds[i].fd = -1;
					break;
				} else {
					printf("Recv: %s, %d Bytes\n", buffer, ret);
				}
				if (--nready == 0) break;
			}
		}
	}
}	

epoll

1.select和poll都是把"添加事件"和"监听"做成一步,而epoll是分开的,在连接数稳定的情况,效率是高很多的
在这里插入图片描述
2.select和poll都是要轮询访问所有文件,在连接数量越来越多的情况下,效率越来越低,因此epoll在内核层每个fd设置一个回调函数,只有fd有事件才会触发回调函数加到eventpoll的队列里

epoll的特点

下图是epoll的TCP连接情况,左边是客户端,中间是epoll事件存储fd和响应事件的数据结构,右边是服务端
流程是
图中客户端123是连接状态,客户端45是正在连接状态
客户端1send()->epoll监听到事件产生EPOLLIN->服务端
服务端send()到客户端2->epoll监听到事件产生EPOLLOUT->客户端3
客户端4connect()->epoll监听到事件产生EPOLLIN->服务端调用epoll_ctl()添加红黑树节点
客户端5connect()->epoll监听到事件产生EPOLLIN->服务端调用epoll_ctl()添加红黑树节点
图中只有客户端3没有收发事件,因为在epoll的事件队列并没有客户端2的事件,epoll只需要"添加事件"和"监听"eventpoll队列是否有事件就可以了
在这里插入图片描述
epoll的结构体

struct epoll_event {
    __uint32_t events;      //epoll事件
    epoll_data_t data;      //用户数据
};
typedef union epoll_data
{
  void *ptr;				//自定义数据
  int fd;					//保存socket
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

events的事件集合

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

epoll的接口如下

//创建一个epoll专用的句柄,会占用一个句柄不用记得close(),size是告诉内核要监听多少个socket,但随即版本的更新,数据结构已经变成红黑树,所以size已经没有意义了,但必须大于0
int epoll_create(int size);
//等待事件的产生,epfd是epoll专用的fd,events获取事件的集合,maxevents是events数组的数量,
//timeout为-1,会一直阻塞监听事件,设置0不管有否有事件都会返回,大于0设置等待时间
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//通过epollfd,把监听fd的事件加到红黑树的节点,epfd是epoll专用的fd,
//op的参数有EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL,
//fd是要修改的socket,event是事件类型和自定义的一些参数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll服务端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <netinet/tcp.h>
#include <arpa/inet.h>

#include <errno.h>
#include <sys/epoll.h>

#define BUFFER_LENGTH	1024
#define EPOLL_SIZE		1024

int main(int argc, char *argv[]) {

	if (argc < 2) {
		printf("Paramter Error\n");
		return -1;
	}
	int port = atoi(argv[1]);

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd < 0) {
		perror("socket");
		return -1;
	}

	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(struct sockaddr_in));

	addr.sin_family = AF_INET;
	addr.sin_port = htons(atoi(argv[1]));
	addr.sin_addr.s_addr = INADDR_ANY;

	if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
		perror("bind");
		return 2;
	}

	if (listen(sockfd, 5) < 0) {
		perror("listen");
		return 3;
	}

	//创建epoll句柄
	int epoll_fd = epoll_create(1);
	struct epoll_event ev, events[EPOLL_SIZE] = {0};

	ev.events = EPOLLIN;
	ev.data.fd = sockfd;
	//添加服务端事件和fd
	epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);
	char buffer[BUFFER_LENGTH] = {0};
	while (1) {
	
		//等待事件
		int nready = epoll_wait(epoll_fd, events, EPOLL_SIZE, -1);
		if (nready == -1) {
			printf("epoll_wait\n");
			break;
		}

		int i = 0;
		for (i = 0;i < nready;i ++) {
			//EPOLLIN事件
			if (events[i].events & EPOLLIN) {
			//sockfd是服务端的fd,其他的是客户端的fd,当服务端有事件,那么就是accept事件
				if (events[i].data.fd == sockfd) {	
					struct sockaddr_in client_addr;
					memset(&client_addr, 0, sizeof(struct sockaddr_in));
					socklen_t client_len = sizeof(client_addr);
				
					int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
					if (clientfd <= 0) continue;
		
					char str[INET_ADDRSTRLEN] = {0};
					printf("recvived from %s at port %d, sockfd:%d, clientfd:%d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
						ntohs(client_addr.sin_port), sockfd, clientfd);

					//设置客户端触发类型EPOLLIN和EPOLLET
					//EPOLLET是每次事件触发一次
					ev.events = EPOLLIN | EPOLLET;
					ev.data.fd = clientfd;
					epoll_ctl(epoll_fd, EPOLL_CTL_ADD, clientfd, &ev);
				} else  {
					//获取发生事件的客户端fd
					int clientfd = events[i].data.fd;
					int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
					if (ret < 0) {
						if (errno == EAGAIN || errno == EWOULDBLOCK) {
							printf("read all data");
						}
						
						close(clientfd);
						
						ev.events = EPOLLIN;
						ev.data.fd = clientfd;
						//将客户端fd移除事件
						epoll_ctl(epoll_fd, EPOLL_CTL_DEL, clientfd, &ev);
					} else if (ret == 0) {
						printf(" disconnect %d\n", clientfd);
						
						close(clientfd);

						//将客户端fd移除事件
						ev.events = EPOLLIN;
						ev.data.fd = clientfd;
						epoll_ctl(epoll_fd, EPOLL_CTL_DEL, clientfd, &ev);
						break;
					} else {
						printf("Recv: %s, %d Bytes\n", buffer, ret);
						//读完数据后回客户端信息,
						//设置EPOLLOUT让clientfd回信息给客户端
						ev.events = EPOLLOUT | EPOLLET;
						ev.data.fd = clientfd;
						//把clientfd的事件类型改为EPOLLOUT
						epoll_ctl(epoll_fd, EPOLL_CTL_MOD, clientfd, &ev);
						
					}
				}
			}
			
			//EPOLLOUT事件
			if(events[i].events & EPOLLOUT) {
				int clientfd = events[i].data.fd;
				//发完数据后收客户端信息,
				//设置EPOLLIN让clientfd收客户端信息
				char buf[1024] = {0};
				sprintf(buf,"recv : %s\n", buffer);
				send(clientfd, buf, BUFFER_LENGTH, 0);
				ev.events = EPOLLIN | EPOLLET;
				ev.data.fd = clientfd;
				//把clientfd的事件类型改为EPOLLIN
				epoll_ctl(epoll_fd, EPOLL_CTL_MOD, clientfd, &ev);
			}
		}
	}
}	

1.epoll的ET和LT

LT:EPOLLLT//水平触发
ET:EPOLLET//边沿触发

水平触发是有数据就会触发,数据是连续的就会一直触发,随着版本的更新水平触发已经是默认的
边沿触发是有数据就会触发,数据是连续的只会触发一次,等没有数据到下一次数据才会触发
LT相当与ET的循环,所以LT效率比较低
用处:EPOLLLT处理大块数据不用担心数据会丢失,ET处理小块数据,效率较高,例如服务端listen和accept可能会一直触发,要用LT.在高并发,流量频繁的情况下就要用到EPOLLET

2.为什么send要通过EPOLLOUT触发

EPOLLOUT触发条件是只要缓冲区没满可写的状态就回触发,所以还要加上EPOLLET.
如果直接用send的话有时可能会写不进去

3.reator

reactor原理

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值