系統編程-網絡-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 = '\0';
  printf("%s\n", 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 \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 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 \n", inet_ntop(AF_INET, &sin.sin_addr.s_addr, buf, sizeof(buf)));
		 printf("buf = %s \n", 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. \n");


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

	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 \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

	}

	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 \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 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 \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);    
    }    

    #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)\n", serv_ip);
    #endif

    /*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; 

    char client_commu_recv_data_buf[100]={0};
    char client_commu_send_data_buf[100]= {"I am server\n"};

    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 .... \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);


        #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)\n", 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... \n");
            int bytes_read_done = read(new_client_fd, client_commu_recv_data_buf, sizeof(client_commu_recv_data_buf));
            printf("bytes_read_done = %d \n", bytes_read_done);

            // sleep(10);

            printf("strlen(client_commu_send_data_buf) = %d \n", 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 \n", bytes_write_done);
            if(bytes_write_done < 0){
                if(errno == EPIPE){
                    printf("server : write -> EPIPE \n");
                    close(new_client_fd);
                    exit(0);
                }
            }
            printf("--Server deal this client over! \n");
            close(new_client_fd);
            exit(0);

        }else{ // parent process

            close(new_client_fd);
        }
    }

    // the following code will nerver run ....
    printf("server process end... \n");
    close(server_local_fd);

    return 0;
}            

 

 

 

 

 

 

 

 

.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM