zoukankan      html  css  js  c++  java
  • 系统编程-网络-tcp客户端服务器编程模型、socket、htons、inet_ntop等各API详解、telnet测试基本服务器功能、getsockname/getpeername

     

    PART1 基础知识

    1. 字节序

    网络字节序是大端字节序(低地址存放更高位的字节), 所以,对于字节序为小端的机器需要收发网络数据的场景,要对这些数据进行字节序转换。

    字节序转换函数,常用的有四个:

    很好记,n表示network, h表示host, l表示long, s表示short。 

    举例, htons 表示将主机的二字节数据转为网络字节序。

     

     

     

    PART2 TCP客户端、服务器 的编程模型 总体概述 以及涉及到的API详解

     

    1. socket套接字的背景介绍

    注意,这里相关描述的组成有五个部分,这就是广为人知的 socket 五元组 =》{协议、本地地址、本地端口、远程地址、远程端口}

     

    socket在内核中的位置如下图

     

    2. socket系统调用正式登场

    对于socket的第二个参数type,大家一般知道的和最常用的只是对应TCP和UDP的两种,实际上一共有4个可选参数哦!

     

    3. 网络通信,肯定需要IP地址,本步骤就是填充好客户端或服务器编程所需的地址信息参数(两种方式)

    方式1 通用地址结构, 使用不方便 ,介绍如下

     

    方式2 因特网地址结构,使用更加方便,介绍如下

    填充ipv4地址 -- 使用示例 (指定服务器所使用的网卡IP为192.168.2.1) : 

     

    在填充struct sockaddr_in类型的结构体变量sin的部分成员时,我们使用到了辅助函数inet_pton。

    功能: 将点分十进制的ip地址转化为用于网络传输的数值格式
    返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1 .

     

    功能: 将数值格式转化为点分十进制的ip地址格式
    返回值:若成功则为指向结构的指针,若出错则为NULL

    好记忆, inet_pton中的p表示pointer,即点分十进制的字符串,而n表示network,即网络字节序的数据。

    所以inet_pton表示将点分十进制的字符串转为网络字节序数据, inet_ntop表示将网络字节序转为点分十进制数据。

    详解:

    (1)这两个函数的af,即family参数,既可以是AF_INET(ipv4), 也可以是AF_INET6(ipv6).
    如果,以不被支持的地址族作为family参数,这两个函数都会返回一个错误,并将errno置为EAFNOSUPPORT.
    (2)inet_pton函数尝试转换由src指针所指向的字符串,并通过dst指针存放二进制结果,
    成功则返回值为1.失败则返回值为0,例如所指定的family不是有效的表达式格式时.
    (3)inet_ntop函数进行相反的转换=》从数值格式(src)转换到表达式(dst)。
    inet_ntop函数的dst参数不可以是一个空指针,调用者必须为目标存储单元分配内存并指定其大小,调用成功时,这个指针就是该函数的返回值。
    size参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。如果size太小,不足以容纳表达式结果,那么返回一个空指针,并置为errno为ENOSPC。

     

    PS:在上图示例代码内,指定了服务器所使用的网卡IP为192.168.2.1,我们也可以监听服务器所在主机的所有网卡,即使用INADDR_ANY,如下图所示

     

    4.  对于服务器编程,接着需要调用bind来绑定上一步骤内准备好的地址信息

    返回:成功则返回 0, 出错返回 -1 。

     

    5. 对于服务器编程,下一步就是让服务器端监听客户端连接,涉及listen系统调用

    listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。

    listen()成功返回0,失败返回-1。

     

    6. 对于服务器编程,接着就是让服务器调用accept()接受客户端的连接

    如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。即:若没有客户端连接,调用此函数会阻塞。

    addr是一个传出参数,accept()返回时传出客户端的地址和端口号。如果给addr参数传NULL,表示不关心客户端的地址。

    addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的缓冲区的长度以避免缓冲区溢出问题,  传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区).

    accept 使用说明1:

    PS: 这里同时获取了连接上来的客户端的信息,使用下图方式可以解析出该信息

     

    accept使用说明2:

    我们的服务器程序结构如下

    while (1) {
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
    n = read(connfd, buf, MAXLINE);
    ......
    close(connfd);
    }

    整个是一个while死循环,每次循环处理一个新客户端的连接。
    由于cliaddr_len是一个传入传出参数(value-result argument), 传入的是调用者提供的缓冲区的长度以避免缓冲区溢出问题,  传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区).所以,每次调用accept()之前应该重新赋初值。
    accept()的参数sockfd一直都是服务器本地负责监听的文件描述符,而accept()的返回值是针对新连接上来的客户端的文件描述符connfd,
    之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开与该客户端的连接。

     

    7. 对于服务器编程,下一步就可以调用read、write系统调用进行数据传输了

    7.1 write函数

    write函数将buf中的nbytes字节内容写入到文件描述符中,成功返回写的字节数,失败返回-1.并设置errno变量。在网络程序中,当我们向套接字文件描述舒服写数据时有两种可能: 

    7.1.1、网络络编程中写函数是不负责将全部数据写完之后再返回的,说不定中途就返回了! 

    write的返回值大于0,表示写了部分数据或者是全部的数据。

    一般用一个while循环不断的写入数据,循环过程中的buf参数和nbytes参数是需要我们来把控的。 

    下面展示一个发送大数据量时的核心代码,准确可靠的

     

    7.1.2、write函数返回值小于0,表示出错了,可查看errno,需要根据错误类型进行相应的处理。 
    如果错误码是EINTR,表示在写的时候出现了中断错误,如果错误码是EPIPE,表示网络连接出现了问题。

     

    7.2 read函数

    read函数是负责从fd中读取内容,当读取成功时,read返回实际读取到的字节数,如果返回值是0,表示已经读取到文件的结束了,小于0表示是读取错误。 
    如果错误是EINTR,表示在写的时候出现了中断错误,如果错误码是EPIPE,表示网络连接出现了问题。

     

    7.3 recv 和 send 函数

    前面的三个参数和read、write函数是一样的。

    第四个参数可以是0或者是以下组合

    MSG_DONTROUTE:不查找表 
    send函数使用的标志,这个标志告诉IP,目的主机在本地网络上,没有必要查找表,这个标志一般用在网络诊断和路由程序里面。 

    MSG_OOB:接受或者发生带外数据 
    表示可以接收和发送带外数据。 

    MSG_PEEK:查看数据,并不从系统缓冲区移走数据 
    recv函数使用的标志,表示只是从系统缓冲区中读取内容,而不清楚系统缓冲区的内容。这样在下次读取的时候,依然是一样的内容,一般在有多个进程读写数据的时候使用这个标志。 

    MSG_WAITALL:等待所有数据 
    recv函数的使用标志,表示等到所有的信息到达时才返回,使用这个标志的时候,recv返回一直阻塞,直到指定的条件满足时,或者是发生了错误。 

     

    8.  close 关闭文件描述符

    成功则返回0,错误返回 -1,

    错误码errno为EBADF,表示fd不是一个有效描述符; 错误码errno为EINTR,表示close函数被信号中断;而EIO则表示一个IO错误。

     

    9. 客户端建立连接

    成功则返回0, 出错返回 -1,  并设置errno。

    客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于服务器bind的参数是服务器自己的地址,而客户端connect的参数是服务器的地址。

     

    PART3 基本服务器端程序编写、使用telnet客户端,针对自己编写好的服务器,进行功能测试

    如果遇到上述问题,解决很简单,按下图开启telnet client功能即可。

     

    实验代码:

    server.h

    #ifndef __SERVER_H__
    
    #define __SERVER_H__
    
    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>    /* superset of previous */
    
    
    #define SERV_PORT     5001
    #define SERV_IPADDR   "192.168.1.21"
    #define QUIT_STR      "quit"
    
    #endif

    server.c

    #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>
    
    
    void my_itoa(long i, char *string)
    {
      int power = 0, j = 0;
    
      j = i;
      for (power = 1; j>10; j /= 10)
    	  power *= 10;
    
      for (; power>0; power /= 10)
      {
    	  *string++ = '0' + i / power;
    	  i %= power;
      }
      *string = '';
      printf("%s
    ", string);
    }
    
    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 1 
    	 // 方式一
    	 sin.sin_addr.s_addr = inet_addr(SERV_IPADDR); 
    	#endif
    
    	#if 0
    	 // 方式二: 
    	 sin.sin_addr.s_addr = INADDR_ANY; 
    	#endif
    
    	#if 0
    	 // 方式三: inet_pton函数来填充此sin.sin_addr.s_addr成员 
    	 if(inet_pton(AF_INET, "192.168.1.21", &sin.sin_addr.s_addr) >0 ){
    		 printf("s_addr=%s 
    ", inet_ntop(AF_INET, &sin.sin_addr.s_addr, buf, sizeof(buf)));
    		 printf("buf = %s 
    ", buf); // 这两条打印语句结果是一样的, inet_ntop调用成功的返回值就是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; 
    
    	while(1) {
    
    		/*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
    
    	}
    
    	close(new_client_fd);
    	close(server_local_fd);
    
    	return 0;
    }

     

    在windows下确保能够ping通我们的ubuntu,然后开启两个telnet去连接我们的服务器程序,结果如下

    ubuntu内编译好代码,运行结果如下

     

    后记: getsockname与 getpeername

     

    getsockname返回参数sockfd指定的本地IP和端口,当套接字的地址与INADDR_ANY绑定时,除非使用connect或accept,否则函数将不返回本地IP的任何信息,但是端口号可以返回,这在双连接时会有所意义。

    实验 -- 使用getsockname来获取本地服务器的IP

    #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>
    
    #include <sys/types.h>
    #include <sys/wait.h>
    
    #include <errno.h>
    // 在Linux网络编程这块,,胡乱包含过多头文件会导致编译不过。
    //#include <linux/tcp.h>  // 包含下方这个头文件,就不能包含该头文件,否则编译报错。
    #include <netinet/tcp.h> // setsockopt 函数 需要包含此头文件
    
    
    int server_local_fd, new_client_fd;
    
    void sig_deal(int signum){
    
        if(signum == SIGINT){
            close(new_client_fd);
            close(server_local_fd);
            exit(0);
    
        }else if(signum == SIGCHLD){
            wait(NULL);
        }
    }
    
    int main(void)
    {
        struct sockaddr_in sin;
    
        signal(SIGINT,  sig_deal);
        signal(SIGCHLD, 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 1
         // 方式二: 
         sin.sin_addr.s_addr = INADDR_ANY; 
        #endif
    
        #if 0
         // 方式三: 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);    
        }    
    
        #if 0
        struct sockaddr_in server_addr;
        socklen_t serv_len = sizeof(server_addr); 
        getsockname(server_local_fd, (struct sockaddr *)&server_addr, &serv_len);
        
        char serv_ip[20] = {0};
        inet_ntop(AF_INET, &server_addr.sin_addr, serv_ip, sizeof(serv_ip));
        
        printf("server IP (%s)
    ", serv_ip);
        #endif
    
        /*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; 
    
        char client_commu_recv_data_buf[100]={0};
        char client_commu_send_data_buf[100]= {"I am server
    "};
    
        while(1){
    
        /*4. accept阻塞等待客户端连接请求 */
            /****获取连接上来的客户端的信息******/
            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("new 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);
    
    
            #if 1 //使用getsockname来获取本地服务器的IP
            /*
            getsockname返回参数sockfd指定的本地IP和端口,
            当套接字的地址与INADDR_ANY绑定时,除非使用connect或accept,否则函数将不返回本地IP的任何信息,但是端口号可以返回,这在双连接时会有所意义
            */
            struct sockaddr_in server_addr;
            socklen_t serv_len = sizeof(server_addr); 
            getsockname(new_client_fd, (struct sockaddr *)&server_addr, &serv_len);
        
            char serv_ip[20] = {0};
            inet_ntop(AF_INET, &server_addr.sin_addr, serv_ip, sizeof(serv_ip));
        
            printf("server IP (%s)
    ", serv_ip);
            #endif
    
    
    
            pid_t pid = fork();
            if(pid < 0){
                continue;
            
            }else if(0 == pid){ // child process
    
                close(server_local_fd); 
    
                printf("server goes to read... 
    ");
                int bytes_read_done = read(new_client_fd, client_commu_recv_data_buf, sizeof(client_commu_recv_data_buf));
                printf("bytes_read_done = %d 
    ", bytes_read_done);
    
                // sleep(10);
    
                printf("strlen(client_commu_send_data_buf) = %d 
    ", strlen(client_commu_send_data_buf));
                int bytes_write_done = write(new_client_fd, client_commu_send_data_buf, strlen(client_commu_send_data_buf));
                printf("bytes_write_done = %d 
    ", bytes_write_done);
                if(bytes_write_done < 0){
                    if(errno == EPIPE){
                        printf("server : write -> EPIPE 
    ");
                        close(new_client_fd);
                        exit(0);
                    }
                }
                printf("--Server deal this client over! 
    ");
                close(new_client_fd);
                exit(0);
    
            }else{ // parent process
    
                close(new_client_fd);
            }
        }
    
        // the following code will nerver run ....
        printf("server process end... 
    ");
        close(server_local_fd);
    
        return 0;
    }            

     

     

     

    .

    /************* 社会的有色眼光是:博士生、研究生、本科生、车间工人; 重点大学高材生、普通院校、二流院校、野鸡大学; 年薪百万、五十万、五万; 这些都只是帽子,可以失败千百次,但我和社会都觉得,人只要成功一次,就能换一顶帽子,只是社会看不见你之前的失败的帽子。 当然,换帽子决不是最终目的,走好自己的路就行。 杭州.大话西游 *******/
  • 相关阅读:
    周总结9
    TDtree冲刺第十天
    规划极限编程阅读笔记03
    TDtree冲刺第九天
    TDtree第八天
    规划极限编程阅读笔记02
    TDtree冲刺第七天
    周总结8
    TDtree冲刺第六天
    11/1
  • 原文地址:https://www.cnblogs.com/happybirthdaytoyou/p/14620161.html
Copyright © 2011-2022 走看看