GoLang NetPoll 实现分析

基石-系统调用

根据《TCP/IP详解 卷2:实现》第十五章插口层,对bind,listen,accept等系统调用作出解释:

  • bind: 将一个本地网络运输层地址和插口联系起来。服务器进程需要绑定到一个已知的地址上,因为客户进程需要同已知地址建立连接或发送数据。
  • listen: 通知协议进程准备接受插口上的连接请求,同时设定可以排队等待的连接数上限,超过上限将拒绝进入排队队列等待。
  • accept: 等待连接请求。返回一个新的描述符,指向一个连接到客户端的新的插口。

在这里插入图片描述

无论如何封装,底层终究离不开这几个基础的系统调用。当socket准备完成后,接下来将面临另外一个问题,如何处理更多的连接;

如何处理请求连接

线性处理

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


#define err_print_exist(m) \
	do { \
		perror(m); \
		exit(EXIT_FAILURE); \
	} while(0);

void handleconn(int conn) {
	char buf[1024];
	while(1) {
		memset(buf, 0, sizeof(buf));
		int r = read(conn, buf, sizeof(buf));
		if (r == 0) {
			printf("client close\n");
			break;
		}else if (r < 0) {
			err_print_exist("read fail");
		} else {
			sleep(1); //假设服务处理逻辑需要耗时1秒
			write(conn, buf, r);
		}
	}
	close(conn);
}


int main(void) {
	//1. fd listen fd
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd < 0) {
		err_print_exist("apply listen fd fail");
	}

	//2. 设置服务端socket结构
	struct sockaddr_in s;
	s.sin_family 	  = AF_INET;
	s.sin_port   	  = htons(1949);
	s.sin_addr.s_addr = inet_addr("192.168.56.105");

	//3. bind
	int r = bind(fd, (struct sockaddr *)&s , sizeof(s));
	if (r < 0) {
		err_print_exist("bind fail");
	}

	//4. listen 
	r = listen(fd, 3);
	if (r < 0) {
		err_print_exist("listen fail");
	}

	//5. accept
	struct sockaddr_in peer;
	socklen_t peerlen = sizeof(peer);
	int conn;

	while (1) {
		conn = accept(fd, (struct sockaddr *)&peer, &peerlen);
		if (conn < 0) {
			err_print_exist("accept fail");
		}
        //致命位置
		handleconn(conn);
	}
}
//clang sample.c -o sample

请求连接是一个一个处理的,当第一个请求连接在处理中时,第二个,第三个…都将不会被处理,对于高并发应用是致命的; 但是后续的连接已经在等待队列中了,待accept操作取连接;
在这里插入图片描述

进程处理

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<signal.h>
#include <sys/wait.h>


#define err_print_exist(m) \
	do { \
		perror(m); \
		exit(EXIT_FAILURE); \
	} while(0);

void handleconn(int conn) {
	char buf[1024];
	while(1) {
		memset(buf, 0, sizeof(buf));
		int r = read(conn, buf, sizeof(buf));
		if (r == 0) {
			printf("client close\n");
			break;
		}else if (r < 0) {
			err_print_exist("read fail");
		} else {
			sleep(1); //假设服务处理逻辑需要耗时1秒
			write(conn, buf, r);
		}
	}
	close(conn);
}

void handler(int sig) {
    while (waitpid(-1, NULL, WNOHANG) > 0);
}


int main(void) {
	signal(SIGCHLD, handler);

	//1. fd listen fd
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd < 0) {
		err_print_exist("apply listen fd fail");
	}

	//2. 设置服务端socket结构
	struct sockaddr_in s;
	s.sin_family 	  = AF_INET;
	s.sin_port   	  = htons(1949);
	s.sin_addr.s_addr = inet_addr("192.168.56.105");

	//3. bind
	int r = bind(fd, (struct sockaddr *)&s , sizeof(s));
	if (r < 0) {
		err_print_exist("bind fail");
	}

	//4. listen 
	r = listen(fd, 3);
	if (r < 0) {
		err_print_exist("listen fail");
	}

	//5. accept
	struct sockaddr_in peer;
	socklen_t peerlen = sizeof(peer);
	int conn;

	pid_t pid;

	while (1) {
		conn = accept(fd, (struct sockaddr *)&peer, &peerlen);
		if (conn < 0) {
			err_print_exist("accept fail");
		}

		pid = fork();
		
		if (pid < 0) {
			err_print_exist("fork process fail");
		} else if (pid == 0) { //子进程
			close(fd);
			handleconn(conn);
			exit(EXIT_SUCCESS);
		} else {  //父进程
			close(conn);
		}

	}
}
//clang process.c -o process

为每个请求连接都分配一个进程处理,确实可以同时处理多个请求连接;同时需要注意更多开发细节,例如进程的创建和退出,避免僵尸进程的产生等
在这里插入图片描述

线程处理

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<signal.h>
#include<pthread.h>


#define err_print_exist(m) \
	do { \
		perror(m); \
		exit(EXIT_FAILURE); \
	} while(0);

void* handleconn(void* fd) {
	int conn = *(int*)fd;

	char buf[1024];
	while(1) {
		memset(buf, 0, sizeof(buf));
		int r = read(conn, buf, sizeof(buf));
		if (r == 0) {
			printf("client close\n");
			break;
		}else if (r < 0) {
			err_print_exist("read fail");
		} else {
			sleep(1); //假设服务处理逻辑需要耗时1秒
			write(conn, buf, r);
		}
	}
	close(conn);

	return 0;
}


int main(void) {
	//1. fd listen fd
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd < 0) {
		err_print_exist("apply listen fd fail");
	}

	//2. 设置服务端socket结构
	struct sockaddr_in s;
	s.sin_family 	  = AF_INET;
	s.sin_port   	  = htons(1949);
	s.sin_addr.s_addr = inet_addr("192.168.56.105");

	//3. bind
	int r = bind(fd, (struct sockaddr *)&s , sizeof(s));
	if (r < 0) {
		err_print_exist("bind fail");
	}

	//4. listen 
	r = listen(fd, 3);
	if (r < 0) {
		err_print_exist("listen fail");
	}

	//5. accept
	struct sockaddr_in peer;
	socklen_t peerlen = sizeof(peer);
	int conn;

	pthread_t thread_id;

	while (1) {
		conn = accept(fd, (struct sockaddr *)&peer, &peerlen);
		if (conn < 0) {
			err_print_exist("accept fail");
		}

		r = pthread_create(&thread_id, NULL, handleconn, (void*)&conn);
		if (r < 0) {
			err_print_exist("pthread create fail");
		}
	}
}
//clang thread.c -o thread -lpthread

每个请求连接都分配一个线程处理,可以同时处理多个请求连接
在这里插入图片描述

线程池处理

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<signal.h>
#include<pthread.h>


#define err_print_exist(m) \
	do { \
		perror(m); \
		exit(EXIT_FAILURE); \
	} while(0);

//线程池部分
typedef struct threadpool_task_s {
	void* (*func)(void*); 	//回调函数
	void* arg;  		//回调函数参数
}threadpool_task_t; 

typedef struct threadpool_s {
	pthread_mutex_t	lock;  		//互斥锁
	pthread_cond_t 	queue_not_full; 
	pthread_cond_t 	queue_not_empty;

	pthread_t	  *threads;
	threadpool_task_t *task_queue;   //任务队列

	int thr_num;   //线程池线程数

	int queue_front;     //任务队列队头下标
	int queue_rear;	     //任务队列队尾下标
	int queue_size;      //任务队列当前任务数
	int queue_max_size;  //任务队列最大任务数
}threadpool_t;

//线程池消费任务
void *threadpool_thread(void* arg) {
	threadpool_t *pool = (threadpool_t *)arg;
    	threadpool_task_t task;

	while(1) {
		pthread_mutex_lock(&(pool->lock));

		while(pool->queue_size == 0) {
			pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock));
		}

		task.func = pool->task_queue[pool->queue_front].func;
		task.arg = pool->task_queue[pool->queue_front].arg;

		pool->queue_front = (pool->queue_front + 1) % pool->queue_max_size;
		pool->queue_size--;

		pthread_cond_broadcast(&(pool->queue_not_full));

		pthread_mutex_unlock(&(pool->lock));

		(*(task.func))(task.arg); //回调函数
	}

	return NULL;

}


//释放线程池
int threadpool_free(threadpool_t *pool) {
	if (pool == NULL) {
		return 0;
	}	

	if(pool->task_queue) {
 		free(pool->task_queue);
	}

	if (pool->threads) {
		free(pool->threads);	
		pthread_mutex_lock(&(pool->lock));
        	pthread_mutex_destroy(&(pool->lock));
        	pthread_cond_destroy(&(pool->queue_not_empty));
        	pthread_cond_destroy(&(pool->queue_not_full));
	}
	free(pool);
	pool = NULL;

	return 0;
}


//创建线程池
threadpool_t* threadpool_create(int thr_num, int queue_max_size) {
	int i;
	threadpool_t *pool = NULL;
	
	do {
		//1. 分配内存初始化线程池结构
		pool = (threadpool_t *)malloc(sizeof(threadpool_t));
		if (NULL == pool) {
			break;
		}

		pool->thr_num = thr_num;
        	pool->queue_size = 0;
        	pool->queue_max_size = queue_max_size;
        	pool->queue_front = 0;
        	pool->queue_rear = 0;

		pool->threads = (pthread_t *)malloc(sizeof(pthread_t) * thr_num);
        	if (pool->threads == NULL) {
        	    break;
        	}
		memset(pool->threads, 0, sizeof(pthread_t) * thr_num);

		pool->task_queue = (threadpool_task_t *)malloc(sizeof(threadpool_task_t) * queue_max_size);
		if (pool->task_queue == NULL) {
            		break;
        	}

 		if (pthread_mutex_init(&(pool->lock), NULL) != 0 || pthread_cond_init(&(pool->queue_not_empty), NULL) != 0 || pthread_cond_init(&(pool->queue_not_full), NULL) != 0) {
                	break;
            	}

		for (i = 0; i < thr_num; i++) {
            		pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void *)pool);
        	}

		return pool;
	}while(0);

	threadpool_free(pool);
	
	return NULL;
}

void dispatch(threadpool_t *pool, void *func(void *arg), void *arg) {
	pthread_mutex_lock(&(pool->lock));

	//任务队列满阻塞分配
	while (pool->queue_size == pool->queue_max_size) {
        	pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock));
    	}

	pool->task_queue[pool->queue_rear].func = func;
	pool->task_queue[pool->queue_rear].arg = arg;

	pool->queue_rear = (pool->queue_rear + 1) % pool->queue_max_size;
	pool->queue_size++;

 	pthread_cond_signal(&(pool->queue_not_empty));
    	pthread_mutex_unlock(&(pool->lock));
}



//线程池
void* handleconn(void* fd) {
	int conn = *(int*)fd;

	char buf[1024];
	while(1) {
		memset(buf, 0, sizeof(buf));
		int r = read(conn, buf, sizeof(buf));
		if (r == 0) {
			printf("client close\n");
			break;
		}else if (r < 0) {
			err_print_exist("read fail");
		} else {
			sleep(1); //假设服务处理逻辑需要耗时1秒
			write(conn, buf, r);
		}
	}
	close(conn);

	return 0;
}


int main(void) {
	//1. fd listen fd
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd < 0) {
		err_print_exist("apply listen fd fail");
	}

	//2. 设置服务端socket结构
	struct sockaddr_in s;
	s.sin_family 	  = AF_INET;
	s.sin_port   	  = htons(1949);
	s.sin_addr.s_addr = inet_addr("192.168.56.105");

	//3. bind
	int r = bind(fd, (struct sockaddr *)&s , sizeof(s));
	if (r < 0) {
		err_print_exist("bind fail");
	}

	//4. listen 
	r = listen(fd, 3);
	if (r < 0) {
		err_print_exist("listen fail");
	}

	//5. accept
	struct sockaddr_in peer;
	socklen_t peerlen = sizeof(peer);
	int conn;

	pthread_t thread_id;

	threadpool_t *pool = threadpool_create(10, 100);
	if (pool == NULL) {
		err_print_exist("thread pool create fail");
	}

	while (1) {
		conn = accept(fd, (struct sockaddr *)&peer, &peerlen);
		if (conn < 0) {
			err_print_exist("accept fail");
		}

		dispatch(pool, handleconn, (void*)&conn);
	}
}

预先创建一批线程,减少线程创建的开销耗时,提高效率,开发复杂度增大
在这里插入图片描述

多路复用

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


#define err_print_exist(m) \
	do { \
		perror(m); \
		exit(EXIT_FAILURE); \
	} while(0);

int handleconn(int conn) {
	char buf[1024];
	memset(buf, 0, sizeof(buf));
	int r = read(conn, buf, sizeof(buf));
	if (r == 0) {
		printf("client close\n");
		close(conn);
		return 0;
	}else if (r < 0) {
		err_print_exist("read fail");
		close(conn);
		return -1;
	} else {
		sleep(1); //假设服务处理逻辑需要耗时1秒
		write(conn, buf, r);
		return 0;
	}
}

int setnonblocking(int fd) {
    return fcntl(fd, F_SETFL, fcntl(fd, F_GETFD, 0)|O_NONBLOCK);
}


int main(void) {
	//1. fd listen fd
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd < 0) {
		err_print_exist("apply listen fd fail");
	}

	//2. 设置服务端socket结构
	struct sockaddr_in s;
	s.sin_family 	  = AF_INET;
	s.sin_port   	  = htons(1949);
	s.sin_addr.s_addr = inet_addr("192.168.56.105");

	//3. bind
	int r = bind(fd, (struct sockaddr *)&s , sizeof(s));
	if (r < 0) {
		err_print_exist("bind fail");
	}

	//4. listen 
	r = listen(fd, 3);
	if (r < 0) {
		err_print_exist("listen fail");
	}

	//5. accept
	struct sockaddr_in peer;
	socklen_t peerlen = sizeof(peer);
	int conn;

	//6. epoll
	int efd = 0;
	
	efd = epoll_create1(0);
	if (efd < 0 ) {
		err_print_exist("epoll create fail");
	}
	struct epoll_event ev, events[0x10];

	ev.events = EPOLLIN;
	ev.data.fd = fd;

	if (epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ev) < 0) {
               err_print_exist("epoll_ctl: listen_sock");
    }

	int nfds, n;

	while (1) {

		nfds = epoll_wait(efd, events, 0x10, -1);
		if (nfds < 0) {
               		err_print_exist("epoll_wait ");
		}

		for (n = 0; n < nfds; ++n) {
			if (events[n].data.fd == fd) {
				conn = accept(fd, (struct sockaddr *)&peer, &peerlen);
				if (conn < 0) {
					err_print_exist("accept fail");
				}
				r = setnonblocking(conn);
				if (r < 0) {
					err_print_exist("conn fd set non blocking fail");
				}
				ev.events = EPOLLIN | EPOLLET;
				ev.data.fd = conn;
				if (epoll_ctl(efd, EPOLL_CTL_ADD, conn, &ev) == -1) {
               				err_print_exist("epoll_ctl add accpet fd fail");
                       		}
			}
			else {
				if (handleconn(events[n].data.fd) < 0 ) {
					epoll_ctl(efd, EPOLL_CTL_DEL, events[n].data.fd, &ev);
				}
			}

		}	
	}
}

IO多路复用是指多个IO复用一个线程; 由复用器epoll完成网络IO的监听(读/写), 然后回调处理逻辑; 还是有些需要注意的点,比如非阻塞网络IO, 回调函数无需循环读取IO,有读写时会触发回调
在这里插入图片描述

Goroutine

package main

import (
	"bufio"
	"fmt"
	"io"
	"net"
)

func handleConn(c net.Conn) {
	defer c.Close()

	var buf = make([]byte, 1024)
	var r = bufio.NewReader(c)

	for {
		n, err := r.Read(buf)
		if err != nil && err == io.EOF {
			return
		}
		if err != nil {
			fmt.Println("read err")
			return
		}
		c.Write(buf[:n])
	}
}

func main() {
	l, err := net.Listen("tcp", "192.168.56.105:1949")
	if err != nil {
		fmt.Println("listen error:", err)
		return
	}

	for {
		c, err := l.Accept()
		if err != nil {
			fmt.Println("accept error:", err)
			break
		}
		go handleConn(c)
	}
}

Go netpoller 通过在底层对 epoll/kqueue/iocp 的封装,从而实现了使用同步编程模式达到异步执行的效果, 简约而不简单; 实现了使用同步编程模式达到异步执行的效果
在这里插入图片描述

引用:
Go netpoller 通过在底层对 epoll/kqueue/iocp 的封装,从而实现了使用同步编程模式达到异步执行的效果。总结来说,所有的网络操作都以网络描述符 netFD 为中心实现。netFD 与底层 PollDesc 结构绑定,当在一个 netFD 上读写遇到 EAGAIN 错误时,就将当前 goroutine 存储到这个 netFD 对应的 PollDesc 中,同时调用 gopark 把当前 goroutine 给 park 住,直到这个 netFD 上再次发生读写事件,才将此 goroutine 给 ready 激活重新运行。显然,在底层通知 goroutine 再次发生读写等事件的方式就是 epoll/kqueue/iocp 等事件驱动机制。

Go netpoller 原生网络模型之源码全面揭秘 文章分析的很深入,细节看这里;

总结

总体来说,请求连接对于服务端程序就是对fd的处理,可以是进程、线程、协程;高并发场景意味着单位时间请求连接多, 所以耗时越短越好,由于创建相同数量的进程、线程和协程“代价”不同,所以“收益”也不同。

参看

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cugriver

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值