用C语言让一个程序同时运行两个服务器,最直接的办法是fork()创建子进程,或者用pthread多线程,再或者用select/epoll事件驱动,其中事件驱动在单线程里就能搞定双端口监听。 你把两个服务器的socket分别绑定不同端口,然后让它们同时进入监听循环,一个程序跑两套服务并不玄乎。
c语言一个程序运行两个服务器的三种主流实现
动手之前先想清楚需求:你要的是进程隔离,还是线程共享,还是单线程扛高并发,行业共识认为,一个C程序同时跑两个服务器的模型逃不出下面三种。
用fork()让父进程和子进程各管一个服务器
fork()最贴近“一个程序跑两个服务”的字面意思,启动时先创建两个socket,分别bind和listen,接着fork一次,子进程继承第二个socket,父进程保留第一个socket,两个进程各跑各的accept循环。
int s1 = create_server(8080);
int s2 = create_server(9090);
pid_t pid = fork();
if (pid == 0) {
server_loop(s2); // 子进程负责9090
} else {
server_loop(s1); // 父进程负责8080
wait(NULL); // 回收子进程
}
这套写法的优势在于两个服务器逻辑完全隔离,一个进程崩溃,另一个进程照常服务,缺点是进程切换开销高,而且子进程退出后如果不wait,会留下僵尸进程。
用pthread多线程同时运行两个服务器
pthread方案适合两个服务器之间需要频繁交换数据的场景,主线程创建两个线程,线程函数里各自做socket监听和处理,代码如下:
pthread_t t1, t2; pthread_create(&t1, NULL, server_thread_8080, NULL); pthread_create(&t2, NULL, server_thread_9090, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL);
注意线程里不能随便用errno,否则会出现难排查的竞争问题,一个线程段错误会直接带崩整个程序,没有fork那种隔离性。
用select或epoll在单线程里处理两个服务器
如果你追求极致资源利用,可以用select把两个监听socket放进同一个fd_set,程序只用一个线程,却同时等待两个端口的连接请求,核心逻辑如下:
fd_set fds;
while (1) {
FD_ZERO(&fds);
FD_SET(s1, &fds);
FD_SET(s2, &fds);
int maxfd = s1 > s2 ? s1 : s2;
select(maxfd + 1, &fds, NULL, NULL, NULL);
if (FD_ISSET(s1, &fds)) handle_accept(s1);
if (FD_ISSET(s2, &fds)) handle_accept(s2);
}
这个方案能轻松应对大量并发连接,但前提是所有socket都必须改成非阻塞模式,如果一直用阻塞read,一个客户端挂住就会连累另一个服务器。
一个程序监听两个端口:C语言双服务器实战(8080和9090)
光讲概念不够,我们直接写一个能跑的示例,需求很简单:同一个C程序,监听8080和9090两个端口,分别返回不同的字符串,这里我用select方案。
步骤1:创建两个socket并绑定端口
两个服务器各调一次socket、bind、listen,bind前一定要设置SO_REUSEADDR,不然程序重启时端口还没释放,绑定会报“Address already in use”,端口一个8080,一个9090,IP都用INADDR_ANY。
步骤2:把两个监听socket放进select
把s1和s2都放进fd_set,select的nfds参数是最大socket描述符加1,这个值算错会导致select无法监视第二个socket,这个坑不少人在初学阶段都踩过。
步骤3:在事件循环里分别处理两个服务器
select返回后,用FD_ISSET判断是哪个socket可读,可读就accept,然后向客户端回一段固定字符串,如果你还要处理大量客户端连接,accept后最好用fork或线程处理每个客户端,否则服务器同时只能服务一个连接。
测试方法很简单:浏览器打开localhost:8080能看到“Server 1”,打开localhost:9090能看到“Server 2”,两个页面都正常,就证明一个C程序成功运行了两个服务器。
多进程和多线程运行两个服务器的性能对比
写代码之前,先看看自己适合哪个模型,下面这张表把常见差异列出来,方便你对照着选:
| 维度 | fork多进程 | pthread多线程 | select/epoll事件驱动 |
|---|---|---|---|
| 资源占用 | 较高,每个进程独立内存 | 中等,共享主进程内存 | 最低,单线程单进程 |
| 隔离性 | 强,一个进程挂了不影响另一个 | 弱,一个段错误全崩 | 一般,主进程崩溃全完 |
| 编程难度 | 简单,但要注意回收子进程 | 中等,要处理锁和线程安全 | 偏高,非阻塞IO理解有门槛 |
| 适合场景 | 两个服务器逻辑完全独立 | 需要共享大量业务数据 | 高并发网关、嵌入式设备 |
就我接触过的Linux网络程序来看,选fork的往往是为了稳,选pthread的为了共享方便,选select/epoll的为了扛高并发,业内专家指出,没有绝对答案,看你最在意什么。
c语言双服务器运行时容易踩的4个坑
坑1:两个服务器想用同一个端口
bind同一个端口第二次调用会失败,除非两个socket都显式设置SO_REUSEPORT,但那是内核负载均衡的玩法,不是通常意义的双服务器,老老实实换端口最省心。
坑2:fork后两个进程同时accept同一个socket
如果你在fork之前只创建了一个socket,然后fork,父子进程会共享那个socket的描述符,两个进程同时accept会导致连接被随机抢走,正确做法是给两个服务器分别建socket再fork。
坑3:多线程里读错errno
线程A调用recv出错,把errno改掉,线程B恰好也在检查errno,结果拿到的是错误值,多线程服务器业务逻辑里应该避免直接依赖全局errno,改用线程参数或线程局部存储来传递错误码。
坑4:select方案忘了非阻塞
单线程里只要有一个客户端连接后不发送数据,阻塞在read上的服务器就卡住了,所有socket都要设置O_NONBLOCK,read返回-1且errno是EAGAIN时,说明这个客户端暂时没数据,先跳过,去处理别的连接。
关于c语言一个程序运行两个服务器常见问题(Q&A)
问:C语言只能创建进程或线程才能运行双服务器吗?
不是,你用select或epoll,一个进程一个线程就能监听两个端口,代码也不会很复杂,这种写法在路由器、智能网关等嵌入式场景里很常见,省内存省CPU。
问:两个服务器需要共享大量业务数据,用什么方案好?
用pthread多线程最合适,线程共享进程内存,直接访问全局结构体,加一把互斥锁就行,用fork就得引入共享内存或消息队列,速度和编程复杂度都不占优势。
问:一个程序运行两个服务器,其中一个服务器崩了怎么办?
如果用的是fork,另一个进程不受影响,这是很多在线业务服务器选择多进程模型的核心原因,如果用的是线程,那只能两个一起挂,所以多线程方案对稳定性要求更高,实际生产中核心服务一般用进程隔离,辅助服务混在同一个进程里也能接受。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/758069.html





