• 系统编程-网络-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 
    ", 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
    ", 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
    ");
            exit(EXIT_FAILURE);
        }
    
        char sendbuf[1024];
        char recvbuf[2014];
    
        while (1)
        {
            fgets(sendbuf, sizeof(sendbuf), stdin);
            printf("strlen(sendbuf) = %d 
    ", strlen(sendbuf));
    
            if (strcmp(sendbuf, "exit
    ") == 0){
                printf("while(1) -> exit 
    ");
                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 
    ");
    
        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 
    ", 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 
    ", inet_ntop(AF_INET, &sin.sin_addr.s_addr, buf, sizeof(buf)));
    		 printf("buf = %s 
    ", 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. 
    ");
    
    
    	char sned_buf[] = "hello, i am server 
    ";
    
    	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 
    ", 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 .... 
    ");
    		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 
    ", ip, port);
    	#endif
    
    	char client_buf[100]={0};
    
    #if 1 // case 1:base function 
    	while(1){
    		printf("server goes to read... 
    ");
    		int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
    		printf("bytes_read_done = %d 
    ", bytes_read_done);
    		usleep(500000);
    	}
    	printf("server process end... 
    ");
    
    	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... 
    ");
    		int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
    		printf("bytes_read_done = %d 
    ", bytes_read_done);
    
    	    close(new_client_fd);
    		while(1);
    	}
    
    	printf("server process end... 
    ");
    	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
    ";
    
    	while(1){
    		printf("server goes to read... 
    ");
    		int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
    		printf("bytes_read_done = %d 
    ", bytes_read_done);
    		if(bytes_read_done <= 0){
    			if(errno == EINTR){
    				/*** 对于EINTR的解释   见下方备注 */
    				printf("network may be ok 
    ");				
    			}
    			else
    			{
    				printf("network is not alive 
    ");
    			}
    		}
    
    	    int bytes = read(new_client_fd, client_buf, sizeof(client_buf));
    		printf("==> bytes = %d 
    ", bytes);
    		if(bytes <= 0){
    			if(errno == EINTR){
    				printf("network may be ok ...
    ");				
    			}
    			else
    			{
    				printf("network is not alive ...
    ");
    			}
    		}
    
    		// 实测,在客户端已经断开连接的情况下,该send函数仍然返回了 strlen(sendbuf)的有效长度。所以,我们不必寄希望于单纯通过send来获取客户端连接状态信息。
    		int bytes_send_done = send(new_client_fd, sendbuf, strlen(sendbuf), 0);
    		printf("bytes_send_done = %d 
    ", bytes_send_done);
    
    		while(1){
    			printf("server is IDLE ... 
    ");
    			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 !
    ");
    
    		}else{
    			printf("client is disconnected !
    ");
    		}
    
    		while(1){
    			printf("server is IDLE ... 
    ");
    			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 判断客户端连接状态, 这种方法实时性高, 推荐使用。

    补充: 也可以采用case 2 的类似方法来判断客户端是否断开连接,即:如果连接已断开,那么write的返回值将小于0,同时,errno会被设置为EPIPE。

     

    相关知识点:

    1.  对于EINTR的解释
    一些IO系统调用执行时,如 read 等待输入期间,如果收到一个信号,系统将中断read, 转而执行信号处理函数.
    当信号处理返回后, 系统遇到了一个问题: 是重新开始这个系统调用, 还是让系统调用失败?
    早期UNIX系统的做法是, 中断系统调用,并让系统调用失败, 比如read返回 -1, 同时设置 errno 为EINTR.
    中断了的系统调用是没有完成的调用,它的失败是临时性的,如果再次调用则可能成功,这并不是真正的失败.
    所以要对这种情况进行处理。

    2. 

    在Linux网络编程这块,胡乱包含过多头文件会导致编译不过。
    //#include <linux/tcp.h> // 包含下方这个头文件,就不能包含该头文件,否则编译报错。
    #include <netinet/tcp.h> // 使用getsockopt、setsockopt函数,需要包含此头文件。

    .

    /************* 社会的有色眼光是:博士生、研究生、本科生、车间工人; 重点大学高材生、普通院校、二流院校、野鸡大学; 年薪百万、五十万、五万; 这些都只是帽子,可以失败千百次,但我和社会都觉得,人只要成功一次,就能换一顶帽子,只是社会看不见你之前的失败的帽子。 当然,换帽子决不是最终目的,走好自己的路就行。 杭州.大话西游 *******/
  • 相关阅读:
    2.3 节的练习
    2.2 节的练习--Compiler principles, technologys, &tools
    web测试点整理(二) -- 输入框
    web测试点整理 -- 注册/登录
    产品测试的思路
    C语言学习--静态链接库和动态链接库
    C语言学习(四)--操作符
    C语言学习(三)--语句
    C语言学习(二)--数据类型
    C语言学习(一)--基本概念
  • 原文地址:https://www.cnblogs.com/happybirthdaytoyou/p/14643074.html
Copyright © 2020-2023  润新知