11-服務端程序終止
這一篇我們將討論伺服器程序終止的問題。
先啟動服務端,再啟動客戶端,然後殺死服務端子程序,以此來模擬伺服器程序終止的情況。如果客戶端再向服務端傳送資料,這將會發生什麼情況?
伺服器程式:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SERV_PORT 10001
////註冊SIGCHLD訊號回收子程序
void wait_child(int signo)
{
printf("hello SIGCHILD\n");
//非阻塞方式回收子程序
while (waitpid(0, NULL, WNOHANG) > 0);
}
int main(void)
{
pid_t pid;
int lfd, cfd;
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
char buf[BUFSIZ], clie_IP[BUFSIZ];
int n, i ,n2;
lfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd, (struct sockaddr *)&serv_addr, sizeof (serv_addr));
listen(lfd, 128);
while (1) {
clie_addr_len = sizeof(clie_addr);
cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
printf("client IP:%s, port:%d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)),
ntohs(clie_addr.sin_port));
//建立子程序
pid = fork();
if (pid < 0) {
perror("fork error");
close(cfd);
exit(1);
} else if (pid == 0) {
close(lfd);
break;
} else {
//父程序
close(cfd);
//捕捉SIGCHLD訊號
signal(SIGCHLD, wait_child);
}
}
//子程序
if (pid == 0) {
while (1) {
n = read(cfd, buf, sizeof(buf));
if (n == 0) {
printf("peer close\n");
//如果read返回0,說明對端已關閉,列印退出客戶端資訊
printf("client IP:%s, port:%d ----- exit\n",
inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)) ,
ntohs(clie_addr.sin_port));
close(cfd);
break; //子程序退出
} else if (n == -1) {
perror("read error");
close(cfd);
break;
} else {
for (i = 0; i < n; i++){
buf[i] = toupper(buf[i]);
}
n2 = write(cfd, buf, n);
if(n2 < n){
puts("short write");
}
}
}
}
close(lfd);
return 0;
}
客戶端程式:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdlib.h>
#define SERV_IP "192.168.0.107"
#define SERV_PORT 10001
int main(void) {
int sfd, len , ret;
struct sockaddr_in serv_addr;
char buf[BUFSIZ];
sfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);
serv_addr.sin_port = htons(SERV_PORT);
ret = connect(sfd, (struct sockaddr *)&serv_addr , sizeof(serv_addr));
if(ret < 0){
perror("connect error:");
close(sfd);
exit(0);
}
//迴圈讀寫資料
while (1) {
fgets(buf, sizeof(buf), stdin);
write(sfd, buf, strlen(buf));
len = read(sfd, buf, sizeof(buf));
//服務端的RST是什麼時候到達,如果是在read返回前到達,那麼read將返回錯誤
//如果是在read之後到達,那麼read將返回0,判斷為對端已關閉
if(len == 0){
puts("peer close");
break;
}else if(len < 0){
perror("read error: ");
}
write(STDOUT_FILENO, buf, len);
}
//關閉連結
close(sfd);
return 0;
}
1 . 先啟動服務端
2.然後執行./client命令,啟動客戶端,並在客戶端輸入hello,然後服務端回射HELLO,驗證客戶端和服務端之間通訊正常
3.然後將服務端程序切換到後臺執行,並找到子程序,通過kill命令將子程序殺死。
當通過kill命令把服務端子程序終止後,父程序收到SIGCHLD訊號後回收了子程序,並且子程序開啟的所有套接字都被關閉
,這將導致服務端子程序將傳送了一個FIN,同時客戶端響應一個ACK,這只是代表服務端到客戶端的連線關閉了而已,也就是所謂的半關閉,進入了FIN_WAIT2狀態。
從tcpdump抓取到的資料包來看,服務端子程序確實傳送了FIN,並接收到了客戶端響應的ACK。
對此,客戶端並不做任何事情,但問題是客戶端將阻塞在read呼叫處,等待從終端讀取資料。此時我們在客戶端的終端上輸入world,因為此時只是半關閉,所以tcp允許這樣做。
當服務端子程序的tcp收到客戶端的world時,於是就響應一個RST段(這是因為之前開啟的套接字服務端子程序已經終止掉了,無法再接收資料了,此時服務端期望對端下一個傳送FIN,而不是傳送一個數據報文,所以才會以RST響應)。我們通過tcpdump檢視發現,確實驗證了這一點,重點關注最後4個報文。
但是對於客戶端來說,它並不知道這個RST,因為它執行的太快了,在呼叫了write後又立即呼叫了read,也就是說在收到對端傳送的RST段之前,它所呼叫的read就已經返回了,所以read返回0得出對端已經關閉
,於是列印peer closed,退出迴圈,執行 close(sockfd)。然後向對端傳送 FIN 段,然後服務端以一個RST響應了客戶端的FIN。