系统编程-网络-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函数,需要包含此头文件。

 

 

 

.