使用State Threads实现简单的服务器

一.源码编译

下面是在Fedora 20(装在了虚拟机中)上的实操记录:
1.从官网State Threads Library download | SourceForge.net下载源码包,最新版是1.9
2.下载完st-1.9.tar.gz,然后解压

tar zxvf st-1.9.tar.gz
cd st-1.9
make

此时会提示“Please specify one of the following targets”,如下图所示:

我选择的是linux-debug。

make linux-debug

此时会在目录st-1.9中产生一个新的目录LINUX_3.11.10-301.fc20.i686_DBG,里面有生成的中间文件*.o, 头文件st.h,libst.so,libst.a和example中的三个例子:lookupdns,proxy,server。
需要注意的是st.h是动态生成的,这种方法值得学习。

二.doc目录研究

在st-1.9源码中doc目录有几个文档,可以参考:
st.html——ST库概论,翻译在网络架构库:State Threads
timeout_heap.txt——超时heap实现
notes.html——给出了编程注意点,包括移植,信号,进程内同步,进程间同步,非网络IO,超时处理,特别谈到进程内同步非常简单,不需要同步资源;非网络IO中谈到drawback和设计时需要避免的方法
reference.html——一个API接口文档介绍,需要认真阅读和熟悉,但是需要编码实战来加深理解
对于reference.html,最重要的是文尾的Program Structure部分,它给出了在一个网络应用程序中使用ST库的基本步骤:
1.如果需要,使用下面的pre-init(预初始化)函数配置ST库,设置时间,事件通知机制
st_set_utime_function()
st_set_eventsys()
2.调用初始化函数st_init()来初始化ST库
3.如果需要,调用post-init(后初始化)函数来配置ST库,设置timecache,随机化线程栈,进程resume和stop的回调函数
st_timecache_set()
st_randomize_stacks()
st_set_switch_in_cb()
st_set_switch_out_cb()
4.生成不同process之间共享的资源,创建并绑定socket,监听socket,生成共享内存段,IPC(进程内通信)channel和同步原语。
st_netfd_open_socket()
st_netfd_serialize_accept()
5.通过fork()创建多进程, 父进程退出或是watchdog
6.在每个子进程中创建thread pool来处理user connection,线程池中的每个线程可以接受客户端连接,也可以连接到其他服务器,或者执行各种network I/O等等
st_thread_create()
st_accept()
st_connect()
st_read()
st_write()
注意:在使用ST库时,只有ST库的I/O函数可以用于network I/O,其他的I/O调用(比如说fread,fwrite)都可能阻塞调用进程。

三.example目录   

      首先阅读里面的README,它简单介绍了这三个例子的基本情况和用法
server包含server.c和error.c
lookupdns包含lookupdns.c和res.c
proxy包含proxy.c
       这里分析server的实现。server接受一个客户端连接,接收客户端数据并返给客户端一个简单的HTML网页(我会做适当修改,让server将接收到的内容原样返回)。以server为基础,我们可以很方便的实现其他的服务器。

     我将源码server.c中的void handle_session(long srv_socket_index, st_netfd_t cli_nfd)函数改成如下形式,这样server会将接收到的内容原样返回,方便测试多个客户端的链接。

void handle_session(long srv_socket_index, st_netfd_t cli_nfd)
{
  char buf[512]={'\0'};
  int n = 0;
  struct in_addr *from = st_netfd_getspecific(cli_nfd);

  if (st_read(cli_nfd, buf, sizeof(buf), SEC2USEC(REQUEST_TIMEOUT)) < 0) {
    err_sys_report(errfd, "WARN: can't read request from %s: st_read",
		   inet_ntoa(*from));
    return;
  }
 n = strlen(buf);
  if (st_write(cli_nfd, buf, n, ST_UTIME_NO_TIMEOUT) != n) {
    err_sys_report(errfd, "WARN: can't write response to %s: st_write",
		   inet_ntoa(*from));
    return;
  }

  RQST_COUNT(srv_socket_index)++;
}

作为Qt的忠实粉丝,我在st-1.9源码目录中新建一个Qt工程,pro文件如下,这样就可以愉快的调试了。

TEMPLATE = app
CONFIG += console
CONFIG -= app_bundle
CONFIG -= qt
TARGET = MyServer

INCLUDEPATH += LINUX_3.11.10-301.fc20.i686_DBG

HEADERS += LINUX_3.11.10-301.fc20.i686_DBG/st.h

SOURCES += examples/server.c \
           examples/error.c

LIBS += LINUX_3.11.10-301.fc20.i686_DBG/libst.a 

1.测试非daemon模式(即interactive模式)下的网络通信

MyServer在运行时需要解析参数,所以在Qt Creator中添加参数如下:

从源码中可以看出-i用了设置interactive模式,后边的参数不管是不是1,都会设置成功。

interactive模式比较简单,不会创建虚拟处理器(VP),也不会写日志。

从static void *handle_connections(void *arg)函数中可以看出,处理完会话后,会将当前socket关闭。该函数实际上实现了一个线程池,最小线程数是max_wait_threads,最大线程数是max_threads。

static void *handle_connections(void *arg)
{
  st_netfd_t srv_nfd, cli_nfd;
  struct sockaddr_in from;
  int fromlen;
  long i = (long) arg;

  srv_nfd = srv_socket[i].nfd;
  fromlen = sizeof(from);

  while (WAIT_THREADS(i) <= max_wait_threads) {
    cli_nfd = st_accept(srv_nfd, (struct sockaddr *)&from, &fromlen,
     ST_UTIME_NO_TIMEOUT);
    if (cli_nfd == NULL) {
      err_sys_report(errfd, "ERROR: can't accept connection: st_accept");
      continue;
    }
    /* Save peer address, so we can retrieve it later */
    st_netfd_setspecific(cli_nfd, &from.sin_addr, NULL);

    WAIT_THREADS(i)--;
    BUSY_THREADS(i)++;
    if (WAIT_THREADS(i) < min_wait_threads && TOTAL_THREADS(i) < max_threads) {
      /* Create another spare thread */
      if (st_thread_create(handle_connections, (void *)i, 0, 0) != NULL)
	WAIT_THREADS(i)++;
      else
	err_sys_report(errfd, "ERROR: process %d (pid %d): can't create"
		       " thread", my_index, my_pid);
    }

    handle_session(i, cli_nfd);

    st_netfd_close(cli_nfd);//关闭socket
    WAIT_THREADS(i)++;
    BUSY_THREADS(i)--;
  }

  WAIT_THREADS(i)--;
  return NULL;
}

客户端,可以分分钟用Qt写一个,就放在windows上吧,运行效果如下所示,我启动了两个客户端:


之所以会弹提示框,是因为检测到MyServer将socket关闭了。

MytcpClient下载地址:MyTcpClient-QT代码类资源-CSDN下载

2.测试daemon模式

       daemon模式下,server创建一个固定数量的进程(“virtual processors”虚拟处理器或VP),并在它们死亡时管理它们。每个虚拟处理器管理它自己的独立的一组state threads(ST:状态线程),其数量各不相同,取决于server的负载。每个VP只监听一个套接字。最初的进程(即daemon进程)成为watchdog(监督者),等待其子进程(也就是VP)死亡或请求终止或重新启动的信号。收到重启信号(SIGHUP)后,所有VP关闭然后重新打开日志文件和重新加载配置。所有当前活动的连接都保留活性。这里假定新配置只影响请求处理而不是服务器参数——例如VP的数量,线程限制,绑定地址等。这些参数被通常被指定为命令行参数,所以服务器为了改变它们必须停止,然后再次启动。

       每个状态线程循环处理来自单个socket的监听。一次只有一个ST在VP上运行,而VP之间不共享内存,所以任何数据都不需要互斥锁,服务器可以自由使用所有的静态变量和非重入库的函数,这大大简化了编程和调试,并提高性能(例如,对于++和---全局计数是安全的或调用inet_ntoa()不需要使用互斥)。每个VP中的当前线程负责保证那个VP的均衡,该线程可以开始一个新线程或终止自身——当备用线程数量超过了上限或者下限。

       这个模式涉及到了多进程(fork)、守护进程(daemon)、进程间通信(signal)等linux知识,还是比较复杂的。在Qt Creator中使用参数“-l ./”就能以daemon模式启动MyServer,这里“-l ./”用来设置日志的路径,因为daemon模式下会打印日志。

daemon模式框架图

       这个图是值得我们学习的,在进行服务器开发的时候,通常用守护进程来管理子进程,真正干活的是子进程。这样做的好处是当一个子进程挂了,不影响服务器的功能,因为守护进程不处理事务,因而挂掉的可能性要小很多。

启动守护进程

static void start_daemon(void)
{
  pid_t pid;

  /* Start forking */
  if ((pid = fork()) < 0)
    err_sys_quit(errfd, "ERROR: fork");
  if (pid > 0)
    exit(0);                  /* parent */

  /* First child process */
  setsid();                   /* become session leader */

  if ((pid = fork()) < 0)
    err_sys_quit(errfd, "ERROR: fork");
  if (pid > 0)                /* first child */
    exit(0);

  umask(022);

  if (chdir(logdir) < 0)
    err_sys_quit(errfd, "ERROR: can't change directory to %s: chdir", logdir);
}

创建并管理子进程

static void start_processes(void)
{
  int i, status;
  pid_t pid;
  sigset_t mask, omask;

  if (interactive_mode) {
    my_index = 0;
    my_pid = getpid();
    return;
  }
 
  for (i = 0; i < vp_count; i++) {
    if ((pid = fork()) < 0) {
      err_sys_report(errfd, "ERROR: can't create process: fork");
      if (i == 0)
	exit(1);
      err_report(errfd, "WARN: started only %d processes out of %d", i,
		 vp_count);
      vp_count = i;
      break;
    }
    if (pid == 0) {
      my_index = i;
      my_pid = getpid();
      /* Child returns to continue in main() */
      return;
    }
    vp_pids[i] = pid;
  }

  /*
   * Parent process becomes a "watchdog" and never returns to main().
   */

  /* Install signal handlers */
  Signal(SIGTERM, wdog_sighandler);  /* terminate */
  Signal(SIGHUP,  wdog_sighandler);  /* restart   */
  Signal(SIGUSR1, wdog_sighandler);  /* dump info */

  /* Now go to sleep waiting for a child termination or a signal */
  for ( ; ; ) {
    if ((pid = wait(&status)) < 0) {
      if (errno == EINTR)
	continue;
      err_sys_quit(errfd, "ERROR: watchdog: wait");
    }
    /* Find index of the exited child */
    for (i = 0; i < vp_count; i++) {
      if (vp_pids[i] == pid)
	break;
    }

    /* Block signals while printing and forking */
    sigemptyset(&mask);
    sigaddset(&mask, SIGTERM);
    sigaddset(&mask, SIGHUP);
    sigaddset(&mask, SIGUSR1);
    sigprocmask(SIG_BLOCK, &mask, &omask);

    if (WIFEXITED(status))
      err_report(errfd, "WARN: watchdog: process %d (pid %d) exited"
		 " with status %d", i, pid, WEXITSTATUS(status));
    else if (WIFSIGNALED(status))
      err_report(errfd, "WARN: watchdog: process %d (pid %d) terminated"
		 " by signal %d", i, pid, WTERMSIG(status));
    else if (WIFSTOPPED(status))
      err_report(errfd, "WARN: watchdog: process %d (pid %d) stopped"
		 " by signal %d", i, pid, WSTOPSIG(status));
    else
      err_report(errfd, "WARN: watchdog: process %d (pid %d) terminated:"
		 " unknown termination reason", i, pid);

    /* Fork another VP */
    if ((pid = fork()) < 0) {
      err_sys_report(errfd, "ERROR: watchdog: can't create process: fork");
    } else if (pid == 0) {
      my_index = i;
      my_pid = getpid();
      /* Child returns to continue in main() */
       err_report(errfd, "Test Information");
      return;
    }
    vp_pids[i] = pid;

    /* Restore the signal mask */
    sigprocmask(SIG_SETMASK, &omask, NULL);
  }
}

       这个函数中先创建了vp_count个子进程,然后主进程进入一个for循环中,在循环中等待进程的信号(wait(&status)),wait函数值阻塞的。如果子进程接收到SIGTERM信号时,在static void child_sighandler(int signo)中会退出该子进程。同时程序走到wait函数之后,创建出一个新的子进程,这样子进程的个数始终维持在vp_count个。

       如果是主进程接收到SIGTERM信号,在static void wdog_sighandler(int signo)中会先kill所有的子进程,然后再退出主进程。此时,所有进程退出。我的电脑是四核,vp_count=4,所以一共有五个进程,如下图所示:

       前四列依次是父进程ID(PPID),进程ID(PID),进程组ID(PGID)和会话ID(SID)。第一个进程的父进程ID是1,这个进程就是守护进程,下面四个进程都是子进程,它们的父进程都是第一个进程。

关于SIGTERM信号,可以通过下列指令测试:

kill -SIGTERM 进程ID号

      State Threads在开源流媒体服务器Simple-RTMP-Server(SRS)中已经有了教科书般的应用,详见:GitHub - winlinvip/srs: Please use https://github.com/ossrs/srs

参考链接:在Ubuntu 14.04 64bit上编译并研究State Threads网络线程库源码_雪峰流云的博客-CSDN博客

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
以下是一个简单使用线程实现Web代理服务器的C代码示例: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #define BUFFER_SIZE 4096 #define MAX_THREADS 10 struct thread_data { int client_fd; }; void *handle_request(void *arg) { struct thread_data *data = (struct thread_data *) arg; int client_fd = data->client_fd; char buffer[BUFFER_SIZE]; ssize_t n; // 接收客户端请求 n = recv(client_fd, buffer, BUFFER_SIZE, 0); if (n < 0) { perror("recv"); goto close_connection; } // 解析HTTP请求,获取URL char *url = strstr(buffer, "GET ") + 4; char *end = strstr(url, " HTTP/"); *end = '\0'; // 向目标服务器发起HTTP请求 int server_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr = { .sin_family = AF_INET, .sin_port = htons(80), .sin_addr.s_addr = inet_addr("目标服务器IP地址") }; if (connect(server_fd, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0) { perror("connect"); goto close_connection; } char request[BUFFER_SIZE]; sprintf(request, "GET %s HTTP/1.1\r\nHost: 目标服务器域名\r\n\r\n", url); send(server_fd, request, strlen(request), 0); // 接收目标服务器的HTTP响应,发送回客户端 while ((n = recv(server_fd, buffer, BUFFER_SIZE, 0)) > 0) { send(client_fd, buffer, n, 0); } close_connection: // 关闭连接,销毁线程 close(client_fd); close(server_fd); free(data); pthread_exit(NULL); } int main() { int server_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr = { .sin_family = AF_INET, .sin_port = htons(8080), .sin_addr.s_addr = INADDR_ANY }; if (bind(server_fd, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0) { perror("bind"); exit(EXIT_FAILURE); } if (listen(server_fd, 10) < 0) { perror("listen"); exit(EXIT_FAILURE); } while (1) { int client_fd = accept(server_fd, NULL, NULL); if (client_fd < 0) { perror("accept"); continue; } // 创建新线程处理客户端请求 pthread_t thread_id; struct thread_data *data = malloc(sizeof(struct thread_data)); data->client_fd = client_fd; if (pthread_create(&thread_id, NULL, handle_request, (void *) data) < 0) { perror("pthread_create"); close(client_fd); free(data); } // 控制线程数量,避免过多线程创建和销毁操作 while (pthread_active_count() > MAX_THREADS) { sleep(1); } } return 0; } ``` 该代码使用了POSIX线程库pthread实现了一个简单的Web代理服务器。主线程使用accept函数接受客户端请求,每个新请求都会创建一个新的线程来处理。在处理线程中,使用recv函数接收客户端请求,解析HTTP请求,向目标服务器发起HTTP请求,接收目标服务器的HTTP响应,然后将响应发送回客户端。最后关闭连接,销毁线程。 该代码还使用了线程池技术,控制线程数量,避免过多线程创建和销毁操作,提高了效率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

草上爬

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

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

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

打赏作者

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

抵扣说明:

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

余额充值