系統編程-網絡-tcp客戶端服務器編程模型(續)、連接斷開、獲取連接狀態場景

相關博文:

系統編程-網絡-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函數,需要包含此頭文件。

 

 

 

.