之前的回声服务器端每次只能向一个客户端提供服务,因此,我们将拓展回声服务器端,使其可以向多个客户端提供服务。下图给出了回声服务器端的实现模型。
可以看出每当有客户端请求服务时,服务器端都将创建子进程提供服务,其流程如下:
与很早之前回声服务器端相比,修改的地方主要有两方面:
while (1)
{
//accept
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
//fork
pid = fork();
if (pid == 0)//子进程
{
close(serv_sock);//关闭子进程服务器套接字
//write
while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
write(clnt_sock, buf, str_len);
close(clnt_sock);//关闭子进程客户端套接字
puts("client disconnected...");
return 0;
}
else //父进程
close(clnt_sock);//关闭父进程客户端套接字
}
close(serv_sock); //关闭父进程服务器套接字
}
有的朋友这里可能会迷惑,为什么有这么多close()函数?
fork()函数调用后父进程将服务端套接字与客户端连接的套接字的文件描述符复制给子进程,所以两个文件描述符指向了同一套套接字。只有两个文件描述符都终止时才能销毁套接字,所以需要把无关的套接字描述符关掉。
创建子进程以及关闭不需要的文件描述符是第一点修改的地方。
//准备及注册 sigaction
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0);
......
//信号处理函数
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);//回收子进程
printf("removed proc id: %d \n", pid);
}
仔细研究发现都是上节利用信号处理技术消灭僵尸进程的东西。没错,我们只是把它又写了一遍,这节没有新知识点。
在分解了新增的代码后,服务器端的实现已经呼之欲出了,下面就给出服务器端的完整代码:
#include
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if (argc != 2) {
printf("Usage : %s \n" , argv[0]);
exit(1);
}
//准备及注册 sigaction
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0);
//socket
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
//bind
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
//listen
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
while (1)
{
//accept
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
if (clnt_sock == -1)
continue;
else
puts("new client connected...");
//fork
pid = fork();
if (pid == -1) //子进程创建失败
{
close(clnt_sock);
continue;
}
if (pid == 0) //子进程
{
close(serv_sock); //关闭子进程服务器套接字
//write
while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
write(clnt_sock, buf, str_len);
close(clnt_sock); //关闭子进程客户端套接字
puts("client disconnected...");
return 0;
}
else //父进程
close(clnt_sock); //关闭父进程客户端套接字
}
close(serv_sock); //关闭父进程服务器套接字
return 0;
}
void read_childproc(int sig)//信号处理函数
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);//回收子进程
printf("removed proc id: %d \n", pid);
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端代码与 4实现一个简单TCP服务器端与客户端 中回声客户端完全相同,在此不赘述
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc,char* argv[])
{
int sock;
int str_len,recv_len,recv_cnt;
char message[BUF_SIZE];
struct sockaddr_in serv_adr;
if(argc != 3)
{
printf("Usage : %s \n" ,argv[0]);
exit(1);
}
sock = socket(PF_INET,SOCK_STREAM,0);
if(sock == -1)
error_handling("socket() error");
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));//端口号
if(connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr)) == -1)
error_handling("connect() error");
else
puts("connected......");
while(1)
{
fputs("Input Message(Q to quit): ",stdout);
fgets(message,BUF_SIZE,stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
str_len=write(sock,message,strlen(message));
recv_len = 0;
while(recv_len < str_len)
{
recv_cnt = read(sock,&message[recv_len],BUF_SIZE - 1);
if(recv_cnt == -1)
error_handling("read() error");
recv_len += recv_cnt;
}
message[str_len] = 0;
printf("Message from server: %s",message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行服务器端并打开多个控制台,分别连接服务器端,不同客户端可以同时与服务器端发送信息并回显发送的消息。下图可以发现每受理一个客户端,会输出new client connected,每关闭一次连接,都会输出子进程的ID号。
我们已经实现的回声客户端的数据回声方式为:向服务端传输数据,并等待服务端回复。无条件等待,直到接收完服务端的回声数据后,才能传输下一批数据。
现在我们可以让客户端的父进程负责接收数据,额外创建的子进程负责发送数据。分割后,不同进程分别负责输入和输出,这样,无论客户端是否从服务端接收完数据都可以进程传输,程序的实现更加简单。
分割的对象是回声客户端,父进程只需编写接收数据的代码,子进程只需编写发送数据的代码。
pid = fork();
if (pid == 0)
write_routine(sock, buf);
else
read_routine(sock, buf);
void read_routine(int sock, char *buf)
{
while (1)
{
int str_len = read(sock, buf, BUF_SIZE);
if (str_len == 0)
return;
buf[str_len] = 0;
printf("Message from server: %s", buf);
}
}
void write_routine(int sock, char *buf)
{
while (1)
{
fgets(buf, BUF_SIZE, stdin);
if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
{
shutdown(sock, SHUT_WR);
return;
}
write(sock, buf, strlen(buf));
}
}
完整的客户端如下:
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);
int main(int argc, char *argv[])
{
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if (argc != 3) {
printf("Usage : %s \n" , argv[0]);
exit(1);
}
//socket
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
//connect
if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("connect() error!");
pid = fork();
if (pid == 0)
write_routine(sock, buf);
else
read_routine(sock, buf);
close(sock);
return 0;
}
void read_routine(int sock, char *buf)
{
while (1)
{
int str_len = read(sock, buf, BUF_SIZE);
if (str_len == 0)
return;
buf[str_len] = 0;
printf("Message from server: %s", buf);
}
}
void write_routine(int sock, char *buf)
{
while (1)
{
fgets(buf, BUF_SIZE, stdin);
if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
{
shutdown(sock, SHUT_WR);
return;
}
write(sock, buf, strlen(buf));
}
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
使用并发服务端与分割I/O的客户端进行通信,实现了本节的目标。