协程实现网络服务器

12 篇文章 0 订阅

源码在这里

对于操作系统而言,进程是最小的资源管理单元,线程是最小的执行单元。对于一个线程,在使用中,性能可能会受到以下因素的影响:

  1. 涉及到线程阻塞状态和可运行状态之间的切换
  2. 线程上下文的切换
  3. 同步锁
  4. ……

所以引入协程——更加轻量级的线程。
就像进程和线程的关系一样,一个进程可以拥有多个线程,一个线程可以拥有多个协程。

协程是在用户态执行的,所以不会像线程切换那样消耗资源,使性能得到提升。可以说,线程是一个特殊的函数,这个函数可以在某个地方挂起,并且可以在挂起处继续运行。一个线程内可以由多个这样的特殊的函数在运行,但是一个线程的多个协程的运行是串行的。如果是多核CPU,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内协程却绝对是串行的。毕竟协程仍然是一个函数,一个线程内可以运行多个函数,但是每个函数都是串行运行的。当一个协程运行时,其他的协程必须被挂起。

C语言上下文切换的库ucontext:

typedef struct ucontex {
	stack_t uc_stack;         // 当前上下文要用到的栈
	mcontext_t uc_mctx;       // 跟硬件相关的信息
	struct ucontext *uc_link; //保存当前context结束后继续执行的context记录
	sigset_t uc_sigmask;      // 存的上下文的运行期间要屏蔽的信号集合
}ucontext_t;

// stack_t结构体 
typedef struct {
	void *ss_sp;   // 栈的起始位置
	int ss_size;   // 栈大小
	int ss_flags;  // 标志
}stack_t;
  • int getcontext(ucontext_t *ucp); 获取当前上下文
  • int setcontext(const ucontext_t *ucp); 将当前程序上下文置为ucp指向的上下文
  • void makecontext(ucontext_t *ucp, void (*func)(), int argc, …);修改ucp指向的上下文, 上下文转而指向函数func, argc是传入参数个数, …为传入参数(直接就是参数, 不用指针),也就是构造上下文。
  • int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);保存当前上下文, 切换到新的上下文, 等于是先执行getcontext(oucp) 再执行setcontext(ucp)

写个程序感受一波 getcontext() 和 setcontext()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ucontext.h>

int main() {
  int i;
  ucontext_t ctx;

  getcontext(&ctx);
  printf("i = %d\n",i++);
  sleep(1);
  setcontext(&ctx);
}

先解释一波:首先 getcontext() 获取上下文,然后往下边执行,执行到 setcontext() 就会去执行getcontext() 获取到的上下文,也就是说就会回到 getcontext() 所在位置继续往下执行,从而形成一个循环,循环的打印 i 的值,结果如下:
在这里插入图片描述

再来个程序感受一波 makecontext() :

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ucontext.h>

void fun() {
  printf("fun()\n");
}

int main() {
  int i = 1;
  char *stack = (char*)malloc(sizeof(char) * 8192);
  ucontext_t ctx_main;
  ucontext_t ctx_fun;

  getcontext(&ctx_main);
  getcontext(&ctx_fun);
  printf("i = %d\n",i++);
  sleep(1);
  ctx_fun.uc_stack.ss_sp = stack;
  ctx_fun.uc_stack.ss_size = 8192;
  ctx_fun.uc_stack.ss_flags = 0;
  ctx_fun.uc_link = &ctx_main;

  makecontext(&ctx_fun, fun, 0);

  setcontext(&ctx_fun);

  printf("main exit\n");
  return 0;
}

解释一波:main 函数首先 getcontext(),获取 main 函数的上下文 ctx_main,ctx_fun 在下边使用了 makecontext(),使得 ctx_fun 的上下文变成了 fun()函数,所以程序会先打印一个 i = X,接着去 fun 函数打印 fun() 因为 ctx_fun 的连接的 ctx_main 所以又陷入一个循环,打印完 i 的值,打印 fun()
在这里插入图片描述

来个代码感受 swapcontext();

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ucontext.h>

ucontext_t ctx_f1;
ucontext_t ctx_f2;
ucontext_t ctx_main;

void fun1() {
  printf("fun1() start\n");
  swapcontext(&ctx_f1, &ctx_f2);
  printf("fun1() end\n");
}

void fun2() {
  printf("fun2() start\n");
  swapcontext(&ctx_f2, &ctx_f1);
  printf("fun2() end\n");
}

int main() {
  char stack1[1024 * 8];
  char stack2[1024 * 8];

  getcontext(&ctx_main);
  getcontext(&ctx_f1);
  getcontext(&ctx_f2);

  ctx_f1.uc_stack.ss_sp = stack1;
  ctx_f1.uc_stack.ss_size = 8192;
  ctx_f1.uc_stack.ss_flags = 0;
  ctx_f1.uc_link = &ctx_f2;
  makecontext(&ctx_f1, fun1, 0);

  ctx_f2.uc_stack.ss_sp = stack2;
  ctx_f2.uc_stack.ss_size = 8192;
  ctx_f2.uc_stack.ss_flags = 0;
  ctx_f2.uc_link = &ctx_main;
  makecontext(&ctx_f2, fun2, 0);

  swapcontext(&ctx_main, &ctx_f1);
  printf("main exit\n");
}

解释:
首先初始化 ctx_f1 ,它的后继是 ctx_f2,接着 makecontext() 构造上下文,ctx_f1 的上下文是 fun1() 函数;

初始化 ctx_f2,它的后继是 ctx_main ,接着 makecontext() 构造上下文, ctx_f2 的上下文是 fun2() 函数。

执行流程:首先主函数遇到 swapcontext(&ctx_main, &ctx_f1),切换到 fun1() 函数,进入 fun1() 函数遇到 swapcontext(&ctx_f1, &ctx_f2) ,切换到 fun2() 函数,进入fun2() 函数遇到 swapcontext(&ctx_f2, &ctx_f1) ,返回 fun1()函数,fun1() 函数执行完,去执行 fun1() 函数的后继 fun2() ,fun2() 函数执行完,去执行 fun2() 函数的后继主函数,效果如下:
在这里插入图片描述

----------------------------------------------------分割线----------------------------------------------------

所以现在要实现协程,协程的结构体中应该有:回调函数、协程上下文、协程栈、协程状态。还得有一个协程调度器来控制所有的协程,具体代码如下:

enum State {
  DEAD,
  READY,
  RUNNING,
  SUSPEND
};

struct schedule;
// 协程结构体
typedef struct {
  // 回调函数
  void *(*call_back)(struct schedule *s, void* args); 
  // 回调函数参数
  void *args;
  // 协程上下文
  ucontext_t ctx;
  // 协程栈
  char stack[STACKSZ];
  // 协程状态
  enum State state;
}coroutine_t;

// 协程调度器
typedef struct schedule {
  // 所有协程
  coroutine_t **coroutines;
  // 当前正在运行的协程,如果没有正在运行的协程为 -1
  int current_id;
  // 最大下标
  int max_id;
  // 主流程上下文
  ucontext_t ctx_main;
}schedule_t;

协程还得有以下功能的函数,具体的实现,参照最开始的链接

// 创建协程调度器                                                                                                 
schedule_t *schedule_creat();

// 协程执行函数                       
static void* main_fun(schedule_t *s);

// 创建协程,返回当前协程在调度器的下标
int coroutine_creat(schedule_t *s, void *(*call_back)(schedule_t *,void *args),void *args);

// 让出 CPU                                                                                                       
void coroutine_yield(schedule_t *s);

// 恢复 CPU
void coroutine_resume(schedule_t *s, int id);

// 删除协程
static void delete_coroutine(schedule_t *s, int id);

// 释放调度器
void schedule_destroy(schedule_t *s);

// 判断所有协程都运行完了
int schedule_finish(schedule_t *s);

// 启动协程
void coroutine_running(schedule_t *s, int id);

有了以上函数的功能,使用协程实现网络服务器的具体思路如下:
做到一请求一协程,而不是一请求一线程。因为协程调度器里边是用数组保存的所有的协程,所以可以用一个循环一直去遍历这个数组,当哪个协程需要执行的时候,就执行,如果让出CPU了,就循环往后遍历看下一个。在同步的情况下实现了异步。用一个 current_id 来判断当前协程是否运行(如果不运行 current_id 置为 -1)。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
libevent是一个开源的网络IO框架,它使用了事件驱动的方式来实现高效的网络处理。协程是一种轻量级的线程,可以在单线程中实现多个任务的并发执行,可以用来实现libevent中的并发处理。 在使用协程实现libevent时,我们需要做以下步骤: 1. 定义事件和回调函数 我们需要定义事件和回调函数,用来处理网络连接和数据传输。在协程模型中,我们需要将回调函数封装为一个协程,用来实现并发处理。可以使用协程库(如libco)提供的API来创建协程。 2. 初始化事件循环 我们需要初始化事件循环,用来监听网络事件并调用回调函数。在协程模型中,我们可以使用一个无限循环来实现事件循环,并在每个循环中切换到不同的协程来处理事件。 3. 注册事件回调函数 我们需要注册事件回调函数,使它们可以被事件循环监听到。在协程模型中,我们可以将事件回调函数封装为一个协程,并将其注册到事件循环中。 4. 运行事件循环 最后,我们需要运行事件循环,开始监听和处理网络事件。在协程模型中,我们可以使用协程库提供的API来启动事件循环,并在其中调用切换协程的函数,以实现并发处理。 下面是一个简单的使用协程实现libevent的示例代码: ``` #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include "co.h" #include "event.h" // 回调函数1 void* handle_client(void* arg) { int client_fd = (int)arg; char buf[1024]; int n = recv(client_fd, buf, sizeof(buf), 0); if (n > 0) { buf[n] = '\0'; printf("Received data: %s\n", buf); send(client_fd, buf, n, 0); } close(client_fd); // 切换回事件循环协程 co_yield_ct(); } // 回调函数2 void* handle_accept(void* arg) { int server_fd = (int)arg; struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len); if (client_fd < 0) { perror("accept"); return NULL; } // 创建协程来处理客户端连接 int ret = co_create(handle_client, (void*)client_fd); if (ret < 0) { fprintf(stderr, "co_create error\n"); close(client_fd); return NULL; } // 切换回事件循环协程 co_yield_ct(); } // 初始化事件循环 event_base_t* init_event() { event_base_t* base = event_base_new(); if (!base) { return NULL; } return base; } // 注册事件回调函数 int register_events(event_base_t* base, int server_fd) { // 注册accept事件 event_t* ev_accept = event_new(base, server_fd, EV_READ|EV_PERSIST, handle_accept, (void*)server_fd); if (!ev_accept) { return -1; } event_add(ev_accept, NULL); return 0; } int main() { const char* ip = "0.0.0.0"; int port = 8888; // 创建服务器套接字 int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("socket"); exit(1); } // 绑定服务器地址和端口 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(ip); server_addr.sin_port = htons(port); int ret = bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); if (ret < 0) { perror("bind"); exit(1); } // 开始监听客户端连接 ret = listen(server_fd, 128); if (ret < 0) { perror("listen"); exit(1); } // 初始化事件循环 event_base_t* base = init_event(); if (!base) { fprintf(stderr, "init_event error\n"); exit(1); } // 注册事件回调函数 ret = register_events(base, server_fd); if (ret < 0) { fprintf(stderr, "register_events error\n"); exit(1); } // 运行事件循环 event_base_loop(base, 0); return 0; } ``` 在上面的代码中,我们使用了libco和libevent两个库来实现协程和事件处理。在handle_accept函数中,当有客户端连接时,我们使用co_create函数创建一个新协程来处理客户端连接。在handle_client函数中,我们处理客户端发送的数据,并使用co_yield_ct函数切换回事件循环协程。在main函数中,我们初始化事件循环并注册事件回调函数,并使用event_base_loop函数开始事件循环。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值