系統程式設計-網路-tcp客戶端伺服器程式設計模型(續)、連線斷開、獲取連線狀態場景

一匹夫發表於2021-04-12

相關博文:

系統程式設計-網路-tcp客戶端伺服器程式設計模型、socket、htons、inet_ntop等各API詳解、使用telnet測試基本伺服器功能

接著該上篇博文,我們們繼續,首先,為了內容的完整性和連續性,我們首要的是立馬補充、展示客戶端的示例程式碼。

在此之後,之後我們們有兩個方向:

一是介紹客戶端、伺服器程式設計中一些注意事項,如連線斷開、獲取連線狀態等場景。

一是基於之前的伺服器端程式碼只是基礎功能,在支援多客戶端訪問時將面臨困局,進一步,我們需要介紹伺服器併發程式設計模型。

 

客戶端程式碼

#include <unistd.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netdb.h>
#include<string.h>
#include<errno.h>
#include<stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#define PORT 5001
#define SERVER_IP "192.168.1.21"

void sig_handler(int signo){
    printf("sig_handler=> pid: %d, signo: %d \n", getpid(), signo);
}

// 如果使用ctrl+c 終止該程式,伺服器也會收到斷開連線事件,
//  可見是作業系統底層幫應用程式擦屁股了。

// 直接呼叫close來關閉該連線,會使得伺服器收到斷開連線事件。
int main()
{
    int sockfd;

    struct sockaddr_in server_addr;
    struct hostent *host;
 

    if(signal(SIGPIPE, sig_handler) == SIG_ERR){
    //if(signal(SIGPIPE, SIG_DFL) == SIG_ERR){ // SIGPIPE訊號的預設執行動作是terminate(終止、退出),所以本程式會退出。
        perror("signal error");
    }

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        fprintf(stderr, "Socket Error is %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);

    if (connect(sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)) == -1)
    {
        fprintf(stderr, "Connect failed\n");
        exit(EXIT_FAILURE);
    }

    char sendbuf[1024];
    char recvbuf[2014];

    while (1)
    {
        fgets(sendbuf, sizeof(sendbuf), stdin);
        printf("strlen(sendbuf) = %d \n", strlen(sendbuf));

        if (strcmp(sendbuf, "exit\n") == 0){
            printf("while(1) -> exit \n");
            break;  
        }

        send(sockfd, sendbuf, strlen(sendbuf), 0);

            //recv(sockfd, recvbuf, sizeof(recvbuf), 0);
            //fputs(recvbuf, stdout);

        memset(sendbuf, 0, sizeof(sendbuf));
            //memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(sockfd);
    printf(" client process end \n");

    return 0;
}

  

伺服器程式碼

#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#include <stdint.h>

#include <string.h>
#include "server.h"
#include <assert.h>

#include <sys/types.h>
#include <unistd.h>
#include <signal.h>


// 在Linux網路程式設計這塊,,胡亂包含過多標頭檔案會導致編譯不過。
//#include <linux/tcp.h>  // 包含下方這個標頭檔案,就不能包含該標頭檔案,否則編譯報錯。
#include <netinet/tcp.h> // setsockopt函式需要包含此標頭檔案


int server_local_fd, new_client_fd;

void sig_deal(int signum){

	close(new_client_fd);
	close(server_local_fd);
	exit(1);
}

int main(void)
{
	struct sockaddr_in sin;

	signal(SIGINT, sig_deal);

	printf("pid = %d \n", getpid());

	 /*1.建立IPV4的TCP套接字 */	
	server_local_fd = socket(AF_INET, SOCK_STREAM, 0);
	if(server_local_fd < 0) {
		perror("socket error!");
		exit(1);	
	}

	 /* 2.繫結在伺服器的IP地址和埠號上*/
	 /* 2.1 填充struct sockaddr_in結構體*/
	 bzero(&sin, sizeof(sin));
	 sin.sin_family = AF_INET;
	 sin.sin_port = htons(SERV_PORT);

	#if 0 
	 // 方式一
	 sin.sin_addr.s_addr = inet_addr(SERV_IPADDR); 
	#endif

	#if 0
	 // 方式二: 
	 sin.sin_addr.s_addr = INADDR_ANY; 
	#endif

	#if 1
	 // 方式三: inet_pton函式來填充此sin.sin_addr.s_addr成員 
	 if(inet_pton(AF_INET, "192.168.1.21", &sin.sin_addr.s_addr) >0 ){
		 char buf[16] = {0};
		 printf("s_addr=%s \n", inet_ntop(AF_INET, &sin.sin_addr.s_addr, buf, sizeof(buf)));
		 printf("buf = %s \n", buf);
	 }
	#endif

	 /* 2.2 繫結*/
	if(bind(server_local_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
		perror("bind");
	       	exit(1);	
	}	

	/*3.listen */
	listen(server_local_fd, 5);
        
	printf("client listen 5. \n");


	char sned_buf[] = "hello, i am server \n";

	struct sockaddr_in clientaddr;
	socklen_t clientaddrlen; 



	/*4. accept阻塞等待客戶端連線請求 */
	#if 0
		/*****不關心連線上來的客戶端的資訊*****/

		if( (new_client_fd = accept(server_local_fd, NULL, NULL)) < 0) {

		}else{
			/*5.和客戶端進行資訊的互動(讀、寫) */
			ssize_t write_done = write(new_client_fd,  sned_buf, sizeof(sned_buf));
			printf("write %ld bytes done \n", write_done);

		}
	#else
		/****獲取連線上來的客戶端的資訊******/

		memset(&clientaddr, 0, sizeof(clientaddr));
		memset(&clientaddrlen, 0, sizeof(clientaddrlen));

		clientaddrlen = sizeof(clientaddr);
		/***
		 * 由於cliaddr_len是一個傳入傳出引數(value-result argument), 
		 * 傳入的是呼叫者提供的緩衝區的長度以避免緩衝區溢位問題,  
		 * 傳出的是客戶端地址結構體的實際長度(有可能沒有佔滿呼叫者提供的緩衝區).
		 * 所以,每次呼叫accept()之前應該重新賦初值。
		 * ******/
		if( (new_client_fd = accept(server_local_fd, (struct sockaddr*)&clientaddr, &clientaddrlen)) < 0) {  
			perror("accept");
			exit(1);	
		}

		printf("client connected!  print the client info .... \n");
		int port = ntohs(clientaddr.sin_port);					
		char ip[16] = {0};
		inet_ntop(AF_INET, &(clientaddr.sin_addr.s_addr), ip, sizeof(ip));
		printf("client: ip=%s, port=%d \n", ip, port);
	#endif

	char client_buf[100]={0};

#if 1 // case 1:base function 
	while(1){
		printf("server goes to read... \n");
		int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
		printf("bytes_read_done = %d \n", bytes_read_done);
		usleep(500000);
	}
	printf("server process end... \n");

	close(new_client_fd);
	close(server_local_fd);
#endif

#if 0 // case 2 : 當伺服器close一個連線時,若client端接著發資料。系統會發出一個SIGPIPE訊號給客戶端程式,告知這個連線已經斷開了,不要再寫了。
// SIGPIPE訊號的預設執行動作是terminate(終止、退出),所以client會退出。若不想客戶端退出可以把SIGPIPE設為SIG_IGN

// 在linux下寫socket的程式的時候,如果嘗試send到一個disconnected socket上,就會讓底層丟擲一個SIGPIPE訊號。
// 驗證方法,伺服器這裡收到一次客戶端訊息後,就關閉該客戶端的描述符。然後客戶端內繼續向此socket傳送資料,觀察客戶端內程式碼的執行效果。
	while(1){
		printf("server goes to read... \n");
		int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
		printf("bytes_read_done = %d \n", bytes_read_done);

	    close(new_client_fd);
		while(1);
	}

	printf("server process end... \n");
	close(server_local_fd);
#endif


#if 0 //case 3 : read()返回值小於等於0時,socket連線有可能斷開。此時,需要進一步判斷errno是否等於EINTR, 
    // 如果errno == EINTR,則說明recv函式是由於程式接收到訊號後返回的,socket連線還是正常的,不應close掉該socket連線。
	// 如果errno != EINTR,則說明客戶端已斷開連線,則伺服器端可以close掉該socket連線。

    if(signal(SIGPIPE, SIG_DFL) == SIG_ERR){
        perror("signal error");
    }

	char sendbuf[1024] = "hello i am server\n";

	while(1){
		printf("server goes to read... \n");
		int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
		printf("bytes_read_done = %d \n", bytes_read_done);
		if(bytes_read_done <= 0){
			if(errno == EINTR){
				/*** 對於EINTR的解釋   見下方備註 */
				printf("network may be ok \n");				
			}
			else
			{
				printf("network is not alive \n");
			}
		}

	    int bytes = read(new_client_fd, client_buf, sizeof(client_buf));
		printf("==> bytes = %d \n", bytes);
		if(bytes <= 0){
			if(errno == EINTR){
				printf("network may be ok ...\n");				
			}
			else
			{
				printf("network is not alive ...\n");
			}
		}

		// 實測,在客戶端已經斷開連線的情況下,該send函式仍然返回了 strlen(sendbuf)的有效長度。所以,我們不必寄希望於單純通過send來獲取客戶端連線狀態資訊。
		int bytes_send_done = send(new_client_fd, sendbuf, strlen(sendbuf), 0);
		printf("bytes_send_done = %d \n", bytes_send_done);

		while(1){
			printf("server is IDLE ... \n");
			usleep(500000);
		}
	}
	
	close(new_client_fd);
	close(server_local_fd);

	/*** 對於EINTR的解釋
	 * 一些IO系統呼叫執行時,如 read 等待輸入期間,如果收到一個訊號,系統將中斷read, 轉而執行訊號處理函式. 
	 * 當訊號處理返回後, 系統遇到了一個問題: 是重新開始這個系統呼叫, 還是讓系統呼叫失敗?
	 * 早期UNIX系統的做法是, 中斷系統呼叫,並讓系統呼叫失敗, 比如read返回 -1, 同時設定 errno 為EINTR.
	 * 中斷了的系統呼叫是沒有完成的呼叫,它的失敗是臨時性的,如果再次呼叫則可能成功,這並不是真正的失敗.
	 * 所以要對這種情況進行處理, 
	 ***/
#endif



#if 0 //case 4: 使用 getsockopt 實時判斷客戶端連線狀態 實時性高

	while(1){	

		sleep(10); // 你可以在這10秒內進行操作,讓客戶端程式退出,或者讓其保持正常連線

		struct tcp_info info; 
		int len = sizeof(info); 
		getsockopt(new_client_fd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len); 

		if((info.tcpi_state == TCP_ESTABLISHED)){
			printf("client is connected !\n");

		}else{
			printf("client is disconnected !\n");
		}

		while(1){
			printf("server is IDLE ... \n");
			usleep(500000);
		}
	}
	
	close(new_client_fd);
	close(server_local_fd);	

#endif


	return 0;
}

PS:程式碼中的備註比較重要,請詳細參考。

伺服器程式碼內使用條件編譯,共有4個case. 思路如下。

case  1, 基本伺服器功能,客戶端發資料,伺服器收資料程式碼展示。  

 

case 2 、3、4 都是連線斷開時的一些情況

case 2  展示了伺服器主動關閉socket連線,對客戶端的影響。

case  2,   伺服器在收到客戶端的一包資料後,就關閉該連線。如果客戶端繼續向此連線發資料,那麼將導致客戶端收到13號訊號,即SIGPIPE,該訊號的預設操作是使程式退出。

 

case 3、4 展示了客戶端斷開連線(在客戶端中斷內敲入exit,即可使得客戶端程式退出)後,伺服器端如何判斷該連線是否已斷開的方法。

case  3,   read()返回值小於等於0時,socket連線有可能斷開。此時,需要進一步判斷errno是否等於EINTR。

            如果errno == EINTR,則說明recv函式是由於程式接收到訊號後返回的,socket連線還是正常的,不應close掉該socket連線。

            如果errno != EINTR,則說明客戶端已斷開連線,則伺服器端可以close掉該socket連線。

 

case 4,使用 getsockopt 判斷客戶端連線狀態, 這種方法實時性高, 推薦使用。

 

 

相關知識點:

1.  對於EINTR的解釋
一些IO系統呼叫執行時,如 read 等待輸入期間,如果收到一個訊號,系統將中斷read, 轉而執行訊號處理函式.
當訊號處理返回後, 系統遇到了一個問題: 是重新開始這個系統呼叫, 還是讓系統呼叫失敗?
早期UNIX系統的做法是, 中斷系統呼叫,並讓系統呼叫失敗, 比如read返回 -1, 同時設定 errno 為EINTR.
中斷了的系統呼叫是沒有完成的呼叫,它的失敗是臨時性的,如果再次呼叫則可能成功,這並不是真正的失敗.
所以要對這種情況進行處理。

 

2. 

在Linux網路程式設計這塊,胡亂包含過多標頭檔案會導致編譯不過。
//#include <linux/tcp.h> // 包含下方這個標頭檔案,就不能包含該標頭檔案,否則編譯報錯。
#include <netinet/tcp.h> // 使用getsockopt、setsockopt函式,需要包含此標頭檔案。

 

 

 

.

相關文章