基石-系统调用
根据《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的处理,可以是进程、线程、协程;高并发场景意味着单位时间请求连接多, 所以耗时越短越好,由于创建相同数量的进程、线程和协程“代价”不同,所以“收益”也不同。