6.并发服务器 以及 与之对应的客户端
该代码大致过程,客户端连接服务器,服务器接收链接后,将创建子进程。客户端从终端输入字符,字符会被送到服务器的子进程,子进程得到字符后,再将字符回馈给客户端,客户端将其显示出来。代码比较长因为加入了信号对死亡子进程的处理。
[root@liumengli net]# cat echo_server.c
#include "/programe/net/head.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "signal.h"
#include "sys/types.h"
#include "unistd.h"
#define LISTENQ 10
void str_echo(int);
void install();//安装对由子进程死亡发出的SIGCHLD信号的处理,如果不做处理子进程会变成僵尸进程
void my_op(int, siginfo_t *, void *);
//IP地址被我硬编码到了代码中,端口由输入参数指定
int main(int argc, char ** argv) {
??????? int listenfd, connfd;
??????? pid_t?? child_pid;
??????? socklen_t child_len;
??????? struct sockaddr_in child_socket, serv_socket;
??????? listenfd = socket(AF_INET, SOCK_STREAM, 0);
??????? bzero(&serv_socket, sizeof(serv_socket));
??????? serv_socket.sin_family = AF_INET;
??????? serv_socket.sin_addr.s_addr = htonl(INADDR_ANY);
??????? serv_socket.sin_port = htons(atoi(argv[1]));
??????? bind(listenfd, (struct sockaddr *)&serv_socket, sizeof(serv_socket));
??????? install();
??????? listen(listenfd, LISTENQ);
??????? for(;;) {
??????????????? child_len = sizeof(child_socket);
??????????????? connfd = accept(listenfd, (struct sockaddr *)&child_socket, &child_len);
??????????????? if(connfd == -1) //值得注意的地方
??????????????????????? continue;
??????????????? if((child_pid = fork()) == 0) {
??????????????????????? printf("my pid is:%d\n", getpid());
??????????????????????? close(listenfd);/值得注意的地方
??????????????????????? str_echo(connfd);
??????????????????????? close(connfd);/值得注意的地方
??????????????????????? exit(0);
??????????????? }
??????????????? close(connfd);
??????? }
}
void str_echo(int connfd) {
??????? ssize_t n;
??????? char buf[100];
??????? for(;;) {
??????????????? if((n = read(connfd, buf, 100)) == 0)
??????????????????????? return;
??????????????? write(connfd, buf, n);
??????? }
}
void install() { //安装对死亡子进程的信号
??????? struct sigaction act, old_act;
??????? sigemptyset(&act.sa_mask);
??????? act.sa_flags = SA_SIGINFO;
??????? act.sa_sigaction = my_op;
??????? if(sigaction(SIGCHLD, &act, &old_act) < 0) {
??????????????? printf("install signal failed\n");
??????????????? exit(1);
??????? }
}
void my_op(int signum, siginfo_t * info, void * myact) {//对死亡子进程处理,防止其变成僵尸进程
??????? pid_t pid;
??????? int stat;
??????? pid = wait(&stat);
??????? printf("%d process terminated\n", pid);
??????? return;
}
?
?
[root@liumengli net]# cat echo_client.c
#include "/programe/net/head.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
int main(int argc, char ** argv) {
??????? int sockfd;
??????? struct sockaddr_in serv_socket;
??????? char buf[100];
??????? if(argc != 2) {
??????????????? printf("please input port");
??????????????? exit(1);
??????? }
??????? sockfd = socket(AF_INET, SOCK_STREAM, 0);
??????? bzero(&serv_socket, sizeof(serv_socket));
??????? serv_socket.sin_family = AF_INET;
??????? serv_socket.sin_port = htons(atoi(argv[1]));
??????? inet_pton(AF_INET, "192.168.1.235", &serv_socket.sin_addr);
??????? connect(sockfd, (struct sockaddr_in *)&serv_socket, sizeof(serv_socket));
??????? int n = read(0, buf, 100);//在linux中0是标准输入
??????? buf[n] = '\0';
??????? write(sockfd, buf, n + 1);
??????? read(sockfd, buf, sizeof(buf));
??????? printf("%s\n", buf);
??????? close(sockfd);
??????? exit(0);
}
?
代码不是非常难,如果有linux下信号处理和I/O操作的编程经验,这个程序不难理解。
?
服务器端,首先是定义自己的监听套接口,然后调用listen函数监听,主进程在accept处会由于完成链接队列中没有链接而被挂起,等客户端connect后,主进程会从accept处返回,主进程会调用fork创建一个子进程,子进程开始对链接connfd进行服务。子进程在完成服务后,调用exit死亡,死亡的子进程会想父进程发送SIGHCLD信号,此时父进程会因循环继续挂起在accept处。挂起的父进程会对信号捕获,并调用my_op函数处理信号。一个子进程就彻底处理完毕。
?
客户端,在connect服务器后,进程会在read(0, buf, 100)处等待客户从键盘输入(0就是代表标准输入,默认情况下是键盘。以回车键标志输入结束)。完毕后,通过write(sockfd, buf, n+1)发送给服务器,服务器会返回数据,通过read读取服务器返回数据,并将数据打印。
?
几个值得注意的地方:
1. close(listenfd);/值得注意的地方
str_echo(connfd);
close(connfd);/值得注意的地方
在linux下,任何一个I/O打开,只是在第一次为这个I/O创建描述结构,之后每次的open都只是对这个描述结构引用,并给该引用计数加1,close只会将共享计数减1,等共享计数减成0以后才会将描述结构删除。子进程在创建时候会继承父进程的所有资源,包括其打开的文件和链接,因此所有的链接和文件描述结构的共享计数都会加1,监听接口listenfd共享计数和链接connfd共享计数都会被加1,子进程关闭listenfd和connfd只是减1共享计数。(貌似不自己手动close,子进程在死亡后也会减1,不过我不能确定,最好自己手动添加close)。
?
2.
if(connfd == -1) //值得注意的地方
??????????????????????? continue;
有些系统调用可能永远阻塞系统称之为慢系统调用,也就是这类调用可能永远无法返回,比如accept,只要没有客户端connect那么这类调用就可能永远不会返回,进程将一直被阻塞,同样后面的read也是这种类型。这儿有个原则:当一个进程被慢系统调用阻塞的时候捕获到一个信号,等到信号处理程序返回时,系统调用可能返回一个EINTR错误。只所以采用可能这个词,是因为某些内核会自动重启这些调用,从而使的主程序将继续在这个系统调用处阻塞。当然不是所有内核都会这么处理,所以后面我们要加上检查语句。