TCP時間服務器
1. 流式套接字客戶端/服務器編程
擬實現一個基本的流式套接字客戶端/服務器通信程序。在該程序中,客戶端和服務器將按照如下步驟交互:
- 客戶端向服務器發出日期時間請求字符串,如:%D %Y %A %T 等。
- 服務器從網絡接收到日期請求字符串後,根據字符串格式生成對應的日期時間值返回給客戶端。
服務器端程序
/*************************************************************************
> File Name: timeserver.c
> Author:
> Mail:
> Created Time: Tue 10 Apr 2018 09:32:39 AM CST
************************************************************************/
/*
* 服務器提供的服務:
* 客戶端向服務器發出日期時間請求字符串,格式:%D %Y %A %T等
* 服務器從網絡接收到日期時間請求字符串後,根據字符串格式生成對應的日期時間值返回給客戶端
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXBUF 512
int main(int argc, char **argv)
{
int listen_fd, conn_fd; /*定義監聽套接字和連接套接字*/
struct sockaddr_in serv_addr, client_addr; /*定義服務器地址和客戶端地址*/
socklen_t len;
int port;
time_t t; /*定義time_t類型的變量表示時間*/
struct tm stime; /*定義一個日曆時間的結構體*/
char req[MAXBUF+1] = {0}; /*接收請求字符串*/
char send_time[MAXBUF+1] = {0}; /*發送時間字符串*/
int z;
if(argc < 2) {
printf("Usage: %s portnumber\n", argv[0]);
exit(1);
}
if((port = atoi(argv[1])) < 0) {
printf("portnumber: 0~65535\n");
exit(1);
}
/*
* 創建套接字
*/
if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket fail");
exit(1);
}
/*
* 設置服務器地址
*/
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(port);
/*
* 綁定套接字
*/
if(bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("bind fail");
exit(1);
}
/*
* 監聽
*/
if(listen(listen_fd, 128) == -1) {
perror("listen fail");
exit(1);
}
while(1) {
len = sizeof(client_addr);
/*接收一個客戶端的連接並創建連接套接字*/
if((conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &len)) == -1) {
perror("accept fail");
exit(1);
}
fprintf(stdout, "Server get connection from %s: %d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
/*處理客戶端請求*/
while(1) {
z = read(conn_fd, req, MAXBUF);
if(z == -1) {
perror("read fail");
exit(1);
}
if(z == 0) {
/*z=0說明客戶端關閉連接*/
fprintf(stdout, "client %s has closed the socket\n", inet_ntoa(client_addr.sin_addr));
close(conn_fd);
break;
}
req[z] = '\0';
time(&t);
stime = *localtime(&t);
/*
* 根據請求格式字符串生成相應時間字符串
*/
strftime(send_time, MAXBUF, req, &stime);
z = write(conn_fd, send_time, strlen(send_time));
if(z == -1) {
perror("write fail");
exit(1);
}
}
}
close(listen_fd);
return 0;
}
客戶端程序
/*************************************************************************
> File Name: timeclient.c
> Author:
> Mail:
> Created Time: Tue 10 Apr 2018 11:03:48 AM CST
************************************************************************/
/*
* 客戶端向服務器發送日期時間格式字符串
* 從服務器接收到時間對應的日期字符串後打印輸出
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define MAXBUF 512
int main(int argc, char **argv)
{
int conn_fd; /*創建連接套接字*/
struct sockaddr_in serv_addr, client_addr; /*創建服務器地址和客戶端地址*/
int port;
char req[MAXBUF+1] = {0};
char recv[MAXBUF+1] = {0};
size_t z;
if(argc < 3) {
printf("Usage: %s address portnumber\n", argv[0]);
exit(1);
}
if((port = atoi(argv[2])) < 0) {
printf("portnumber: 0~65535\n");
exit(1);
}
if((conn_fd = socket(AF_INET, SOCK_STREAM,0)) == -1) {
perror("sockt fail");
exit(1);
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(port);
if(connect(conn_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("connect fail");
exit(1);
}
fprintf(stdout, "\tclient connects to %s: %s\n", argv[1], argv[2]);
while(1) {
printf("\nEnter format string('q'uit?):");
if(fgets(req, MAXBUF, stdin) == NULL || req[0] == 'q') {
printf("\n");
break;
}
z = strlen(req);
if(z>0 && req[--z] == '\n') {
req[z] = '\0';
}
if(z == 0)
continue; /*客戶端僅僅輸入換行符*/
/*發送請求到服務器端*/
z = write(conn_fd, req, strlen(req));
if(z<0) {
perror("write fail");
exit(1);
}
fprintf(stdout, "\tclient has sent '%s' to the server\n", req);
/*從服務器端接收*/
if((z = read(conn_fd, recv, MAXBUF)) == -1) {
perror("read fail");
exit(1);
}
if(z == 0) {
fprintf(stdout, "server has closed the socket\n");
fprintf(stdout, "press any key to continue...\n");
getchar();
break;
}
recv[z] = '\0';
/*輸出結果*/
printf("result from %s: %d \n\t'%s'\n",
inet_ntoa(serv_addr.sin_addr),
ntohs(serv_addr.sin_port),
recv);
}
close(conn_fd);
return 0;
}
運行實驗
運行服務器端:
$ ./timeserver 9000
Server get connection from 127.0.0.1: 57628
client 127.0.0.1 has closed the socket
運行客戶端
$ ./timeclient 127.0.0.1 9000
client connects to 127.0.0.1: 9000
Enter format string('q'uit?):%Y %D
client has sent '%Y %D' to the server
result from 127.0.0.1: 9000
'2018 04/10/18'
Enter format string('q'uit?):q
2. 併發流式套接字客戶端/服務器編程
上面的流式套接字服務器在對所接收到的一個客戶端的連接請求進行處理時,不能再接收(執行accept函數)其他客戶端的鏈接請求,只有當服務器完全結束了對這個客戶端的所有請求處理後,才能對下一個客戶端的請求進行處理。
可以利用Linux系統的多任務特性,通過創建子進程系統調用,讓新產生的子進程對客戶端請求進行後續處理,而主進程返回繼續接收其他客戶端發來的請求,這樣就實現了同時對多個客戶端請求的並行處理模式。
服務器程序
/*************************************************************************
> File Name: multi_server.c
> Author:
> Mail:
> Created Time: Tue 10 Apr 2018 01:03:19 PM CST
************************************************************************/
/*
* 這是一個多進程服務器
* 服務器主進程負責監聽
* 子進程負責處理連接請求
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <netdb.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define MAXBUF 512
static void sigchild_handler(int signo)
{
pid_t pid;
int status;
char msg[] = "SIGCHLD caught\n";
write(STDOUT_FILENO, msg, sizeof(msg));
do {
pid = waitpid(-1, &status, WNOHANG);
}while(pid>0);
}
int main(int argc, char **argv)
{
int listen_fd, conn_fd; /*定義監聽套接字和連接套接字*/
pid_t pid; /*進程標識符*/
struct sockaddr_in serv_addr, client_addr; /*定義服務器地址和客戶端地址*/
socklen_t len;
int port;
time_t t;
struct tm stime;
char req[MAXBUF+1] = {0};
char send_time[MAXBUF+1] = {0};
int z;
struct sigaction child_action;
memset(&child_action, 0, sizeof(child_action));
child_action.sa_flags |= SA_RESTART;
child_action.sa_handler = sigchild_handler;
if(sigaction(SIGCHLD, &child_action, NULL) == -1) {
perror("Fail to ignore SIGCHLD");
}
if(argc < 2) {
fprintf(stdout, "Usage: %s portnumbr\n", argv[0]);
exit(1);
}
if((port = atoi(argv[1])) < 0) {
fprintf(stdout, "portnumber: 0~65535\n");
exit(1);
}
/*
* 創建監聽套接字
*/
if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket fail");
exit(1);
}
/*
* 設置服務器地址
*/
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(port);
/*
* 綁定
*/
if(bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("bind fail");
exit(1);
}
/*
* 監聽
*/
if(listen(listen_fd, 128) == -1) {
perror("listen fail");
exit(1);
}
while(1) {
len = sizeof(client_addr);
if((conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &len)) == -1) {
perror("accept fail");
exit(1);
}
fprintf(stdout, "Server get connection from %s: %d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
/*
* 創建子進程進行處理
*/
pid = fork();
if(pid == -1) {
perror("fork fail");
exit(1);
} else if(pid == 0) {
/*
* 子進程處理
*/
fprintf(stdout, "\tEntering the child: %d\n", getpid());
while(1) {
z = read(conn_fd, req, MAXBUF);
if(z == -1) {
perror("read fail");
exit(1);
}
if(z == 0) {
fprintf(stdout, "\tclient %s has close the socket\n",
inet_ntoa(client_addr.sin_addr));
break;
}
req[z] = '\0';
time(&t);
stime = *localtime(&t);
strftime(send_time, sizeof(send_time), req, &stime);
z = write(conn_fd, send_time, strlen(send_time));
if(z == -1) {
perror("write fail");
exit(1);
}
}
fprintf(stdout, "\tChild process: %d exits\n", getpid());
close(conn_fd);
exit(0);
} else {
/*
* 父進程
*/
fprintf(stdout, "This is parent\n");
close(conn_fd); /*關閉重複的套接字*/
}
}
close(listen_fd);
return 0;
}
服務器主進程
- 函數
sigchild_handler
定義了子進程退出信號的處理程序,並且在之後註冊了該信號處理程序。當負責處理客戶端的子進程退出時,將由信號處理程序進行善後處理,避免出現僵死進程。 - 父進程在166行關閉連接套接字。這一步很重要,因爲調用了fork函數後,父進程和子進程都打開了相連的套接字,但是父進程此時並不爲此鏈接客戶端的請求進行具體服務,而是繼續進行監聽,所以它必須關閉此連接套接字,而子進程繼續使用此連接套接字爲客戶端提供服務。
服務器子進程
服務器子進程循環處理客戶端發來的請求,當客戶端請求完畢斷開鏈接後,服務器子進程的read操作將遇到EOF,從而導致子進程退出處理循環並關閉服務器的此連接套接字。子進程執行exit(0)後,內核將向父進程發送SIGCHLD信號,同時子進程進入僵死狀態。
服務器子進程終止
子進程終止時,父進程的相關善後處理:
- 內核提交SIGCHLD信號,說明子進程已經終止;
- sigchild_handler信號處理函數開始執行,通過do…while()循環,調用waitpid直到沒有任何已退出的子進程。另外waitpid使用了WNOHANG參數,因爲信號處理程序不能阻塞,否則無法爲下一個可能到來的客戶端連接服務。
實驗
運行服務器:
$ ./multi_server 9000
Server get connection from 127.0.0.1: 57698
This is parent
Entering the child: 3387
Server get connection from 127.0.0.1: 57700
This is parent
Entering the child: 3389
client 127.0.0.1 has close the socket
Child process: 3389 exits
SIGCHLD caught
client 127.0.0.1 has close the socket
Child process: 3387 exits
SIGCHLD caught
運行客戶端1
$ ./timeclient 127.0.0.1 9000
client connects to 127.0.0.1: 9000
Enter format string('q'uit?):%c
client has sent '%c' to the server
result from 127.0.0.1: 9000
'Tue Apr 10 14:56:23 2018'
Enter format string('q'uit?):q
運行客戶端2
$ ./timeclient 127.0.0.1 9000
client connects to 127.0.0.1: 9000
Enter format string('q'uit?):q