在网络程序里面,一般来说都是许多客户对应一个服务器(多对一),为了处理客户的请求,对服务端的程序就提出了特殊的要求。目前最常用的服务器模型有:
迭代服务器:服务器在同一时刻只能响应一个客户端的请求;
并发服务器:服务器在同一时刻可以响应多个客户端的请求。
在 Linux 环境下多进程的应用很多,其中最主要的就是网络/客户服务器。多进程服务器是当客户有请求时,服务器用一个子进程来处理客户请求。父进程继续等待其它客户的请求。这种方法的优点是当客户有请求时,服务器能及时处理客户,特别是在客户服务器交互系统中。对于一个 TCP 服务器,客户与服务器的连接可能并不马上关闭,可能会等到客户提交某些数据后再关闭,这段时间服务器端的进程会阻塞,所以这时操作系统可能调度其它客户服务进程,这比起迭代服务器大大提高了服务性能。
#include <头文件>
// 信号处理函数,回收子进程
void sig_chld(int signo) { }
int main(int argc, char const *argv[])
{
lfd = socket();
bind();
listen();
signal(); // 发送SIGCHLD信号
while(1)
{
int cfd = accept();
if (fork() == 0)
{
close(lfd); // 子进程关闭父进程的套接字
/*事务处理*/
close(cfd); // 子进程关闭自己的套接字
exit(0); // 子进程退出
}
// 父进程关闭子进程的套接字(子进程文件描述符引用计数变为0,就关闭客户端)
close(cfd);
}
close(lfd);
return 0;
}
每一个socket描述符都有对应的引用计数,该计数存在文件表中。上面程序中打开了lfd和cfd,引用计数分别为1和1,在fork()以后,子进程复制了父进程的socket描述符,所以引用计数也会增加,即lfd和cfd的引用计数都变成了2。此时在子进程中关闭lfd(close(lfd)),在父进程中关闭close(cfd)。这个就保证了子进程处理与客户的连接,父进程负责在监听套接字lfd再次调用accept来接收客户的下一个连接。
从上面分析和图示可以看出socket描述符是有引用计数的,只有当引用计数为0的时候,close才会发送FIN报文,这就解释了子进程仍然可以用cfd进行数据处理了,子进程处理完后再次调用close,此时引用计数从1变为0,最后发送FIN报文结束cfd连接。而对于lfd,只有在子进程中进行了close,父进程中一直保留着引用计数为1,所以父进程通过for循环可以持续accept新连接。
通常调用close函数对socket进行关闭,为啥还要选用shutdown来关闭socket,原因有如下两个:
在介绍close的时候,已经说明了,close只有在对应socket的引用计数为0时,才会真正发送FIN报文来关闭这个连接,shutdown没有这个限制,直接发送FIN报文。
close同时终止了读和写两个方向的数据传输。但是TCP的双工的,我们有时候需要只接受数据,而不发送数据,shutdown可以指定关闭读端或者写端。
int shutdown(int fd, int how);
/*
how为SHUT_RD(关闭读端),则无法从套接字读取数据;-------不发送FIN
how为SHUT_WR(关闭写端),则无法从套接字写数据;---------发送FIN
how为SHUT_RDWR(关闭读写),则无法从套接字读和写数据;-------发送FIN
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void sys_err(const char *str)
{
perror(str);
exit(1);
}
// 信号处理函数
void sig_chld(int signo)
{
pid_t pid;
int stat; // 回收状态
// 以非阻塞形式回收子进程
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
int main(int argc, char **argv)
{
int lfd, cfd;
pid_t pid;
socklen_t clt_addr_len;
struct sockaddr_in srv_addr, clt_addr;
// 将地址结构清零(按字节),容易出错(后面两个参数容易颠倒)
// memset(&srv_addr, 0, sizeof(srv_addr));
// bzero也可以用来清零操作
bzero(&srv_addr, 0);
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(8000);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 创建套接字
lfd = socket(AF_INET, SOCK_STREAM, 0);
// 端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
// 绑定套接字
bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
// 监听客户端的连接
listen(lfd, 128);
clt_addr_len = sizeof(clt_addr);
// 子进程无论是正常退出还是异常退出都会给父进程发送SIGCHLD信号
signal(SIGCHLD, sig_chld);
char buf[512];
while (1)
{
// 阻塞接收客户端的连接
cfd = accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
// 使用fork创建子进程
if ( (pid = fork()) < 0)
sys_err("fork");
// 子进程
else if (pid == 0)
{
// 关闭不需要的套接字可节省系统资源,
// 同时可避免父子进程共享这些套接字,
// 可能带来的不可预计的后果。
close(lfd); // 关闭服务器套接字描述符,这个套接字是从父进程继承过来的
while (1)
{
// 清空数组
memset(buf, 0, 512);
// 接收数据
recv(cfd, buf, sizeof(buf), 0);
int ret = strlen(buf);
if (ret == 0)
{
// 没有接收到数据说明客户端关闭了连接
close(cfd);
exit(1);
}
for (int i = 0; i < ret; ++i)
buf[i] = toupper(buf[i]);
// 回射给客户端
send(cfd, buf, ret, 0);
// 服务器把数据写到标准输出
write(STDOUT_FILENO, buf, ret);
memset(buf, 0, 512);
close(cfd); // 子进程关闭自己的套接字
exit(0); // 子进程退出
}
}
// 父进程
else
{
// 关闭客户端套接字描述符
close(cfd);
continue;
}
}
close(lfd); // 最后关闭服务器套接字描述符
return 0;
}
多线程服务器是对多进程的服务器的改进,由于多进程服务器在创建进程时要消耗较大的系统资源,所以用线程来取代进程,这样服务处理程序可以较快的创建。据统计,创建线程与创建进程要快 10100 倍,所以又把线程称为“轻量级”进程。线程与进程不同的是:一个进程内的所有线程共享相同的全局内存、全局变量等信息,这种机制又带来了同步问题。
#include <头文件>
void *client_process(void *arg)
{
int cfd = *(int *)arg; // 传过来的已连接套接字
/*事务处理*/
close(cfd);
}
int main(int argc, char const *argv[])
{
lfd = socket();
bind();
listen();
while(1)
{
cfd = accept();
pthread_t thread_id;
if (cfd > 0)
{
//给回调函数传的参数,&connfd,地址传递
pthread_create(&thread_id, NULL, client_process, (void *)&cfd); //创建线程
pthread_detach(thread_id); // 线程分离,结束时自动回收资源
}
}
close(lfd);
return 0;
}
pthread_create(&thread_id, NULL, client_process, (void *)&cfd)
在这里我们使用的是按地址传递,所以会有这么一个问题,假如有多个客户端要连接这个服务器,正常的情况下,一个客户端连接对应一个 cfd,相互之间独立不受影响,但是,假如多个客户端同时连接这个服务器,A 客户端的连接套接字为 cfd,服务器正在用这个 cfd 处理数据,还没有处理完,突然来了一个 B 客户端,accept()之后又生成一个 cfd, 因为是地址传递, A 客户端的连接套接字也变成 B 这个了,这样的话,服务器肯定不能再为 A 客户端服务器了,这时候,我们就需要考虑多任务的互斥或同步问题了,这里通过互斥锁来解决这个问题,确保这个cfd值被一个临时变量保存过后,才允许修改。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
pthread_mutex_t mutex;
// 信号处理函数
void *client_process(void *arg)
{
char buf[1024] = ""; // 接收缓冲区
int cfd = *(int *)arg; // 传过来的已连接套接字
// 解锁,pthread_mutex_lock()唤醒,不阻塞
pthread_mutex_unlock(&mutex);
// 接收数据
recv(cfd, buf, sizeof(buf), 0);
int ret = strlen(buf);
if (ret == 0)
{
// 没有接收到数据说明客户端关闭了连接
close(cfd);
exit(1);
}
for (int i = 0; i < ret; ++i)
buf[i] = toupper(buf[i]);
// 回射给客户端
send(cfd, buf, ret, 0);
// 服务器把数据写到标准输出
write(STDOUT_FILENO, buf, ret);
close(cfd); // 子进程关闭自己的套接字
return NULL;
}
int main(int argc, char **argv)
{
int lfd, cfd;
socklen_t clt_addr_len;
struct sockaddr_in srv_addr, clt_addr;
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁,互斥锁默认是打开的
// 将地址结构清零(按字节),容易出错(后面两个参数容易颠倒)
// memset(&srv_addr, 0, sizeof(srv_addr));
// bzero也可以用来清零操作
bzero(&srv_addr, 0);
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(8000);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 创建套接字
lfd = socket(AF_INET, SOCK_STREAM, 0);
// 端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
// 绑定套接字
bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
// 监听客户端的连接
listen(lfd, 128);
clt_addr_len = sizeof(clt_addr);
pthread_t thread_id;
while (1)
{
// 阻塞接收客户端的连接
cfd = accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
if (cfd > 0)
{
//给回调函数传的参数,&connfd,地址传递
pthread_create(&thread_id, NULL, client_process, (void *)&cfd); //创建线程
pthread_detach(thread_id); // 线程分离,结束时自动回收资源
}
}
close(lfd);
return 0;
}
参考:https://blog.csdn.net/tennysonsky/article/details/45671215