基於Linux C的TCP socket編程筆記

一、基本概念

1、socket套接字

對於這個概念我想到了幾個問題:socket是什麼?是對什麼數據結構被操作?
一個socket描述符代表兩個地址對 “本地ip:port” 和 “遠程ip:port”
socket爲內核對象,由操作系統內核來維護其緩衝區,引用計數,並且可以在多個進程中使用。
在使用socket編程時,在網絡通信以前首先要建立連接,而連接是通過對socket的一些操作來完成的。建立連接的過程可以分爲以下幾步:

1) 建立socket套接字

使用socket建立套接字的時候,實際上是建立了一個數據結構。這個數據結構主要的信息是指定了連接的種類和使用的協議,此外還有一些關於連接隊列操作的結構字段。
當使用socket函數以後,如果成功的話會返回一個int型的描述符,它指向前面那個被維護在內核裏的socket數據結構。任何操作都是通過這個描述符而作用到那個數據結構上的。這就像是在建立一個文件後得到一個文件描述符一樣,對文件的操作都是通過文件描述符來進行的,而不是直接作用到inode數據結構上。之所以用文件描述符舉例,是因爲socket數據結構也是和inode數據結構密切相關,它不是獨立存在於內核中的,而是位於一個VFS inode結構中。所以,有一些比較抽象的特性,可以用文件操作來不恰當的進行類比以加深理解。 當建立了這個套接字以後,可以獲得一個像文件描述符那樣的套接字描述符。就像對文件進行操作那樣,可以通過向套接字裏面寫數據將數據傳送到指定的地方,這個地方可以是遠端的主機,也可以是本地的主機,還可以用socket機制來IPC(進程間通信)。

2)給套接字賦地址

依照建立套接字的目的不同,賦予套接字地址的方式有兩種:服務器端使用bind,客戶端使用connetc。

bind:

使用IP, port就可以區分一個TCP/IP連接(這個連接指的是一個連接通道,如果要區分特定的主機間的連接,還需要第三個屬性 hostname)。可以使用bind函數來爲一個使用在服務器端例程中的套接字賦予通信的地址和端口。
在這裏通信的IP地址和端口合起來構成了一個socket地址,而指定一個socket使用特定的IP和port組合來進行通行的過程就是賦予這個socket一個地址。要賦予socket地址,就得使用一個數據結構來指明特定的socket地址,這個數據結構就是struct sockaddr。bind函數的作用就是將這個特定的標註有socket地址信息的數據結構和socket套接字聯繫起來,即賦予這個套接字一個地址。
一個特定的socket的地址的生命期是bind成功以後到連接斷開前。你可以建立一個socket數據結構和socket地址的數據結構,但是在沒有bind以前他們兩個是沒有關係的,在bind以後他們兩個纔有了關係。這種關係一直維持到連接的結束,當一個連接結束時,socket數據結構和socket地址的數據結構還都存在,但是他們兩個已經沒有關係了。如果要是用這個套接字在socket地址上重新進行連接時,需重新bind他們兩個。
bind指定的IP通常是本地IP(一般不特別指定,而使用INADDR_ANY來聲明),而最主要的作用是指定端口。在服務器端的socket進行了bind以後就是用listen來在這個socket地址上準備進行連接。

connect:

對於客戶端來說,是不會使用bind的(並不是不能用,但沒什麼意義),他們會通過connet函數來建立socket和socket地址之間的關係。其中的socket地址是它想要連接的服務器端的socket地址。在connect建立socket和socket地址兩者關係的同時,它也在嘗試着建立遠端的連接。

3) 建立socket連接。

對於準備建立一個連接,服務器端要兩個步驟:bind, listen;客戶端一個步驟:connect。如果服務器端accept一個connect,而客戶端得到了這個accept的確認,則一個連接就建立了。

2、網絡字節序

內存中的多字節數據都有大小端之分,磁盤文件中的多字節數據相對於文件中的偏移地址也有大小端之分,同樣,網絡數據流也有大小端之分。
網絡數據流的地址規定:先發出的數據時低地址,後發出的數據是高地址。發送主機通常將發送緩衝區中的數據按內存地址從低到高的順序發出,爲了不使數據流亂序,接收主機也會把從網絡上接收的數據按內存地址從低到高的順序保存在接收緩衝區中。
TCP/IP協議規定:網絡數據流應採用大端字節序,即低地址高字節。

由於兩端的兩個主機的大小端不一定相同,因此爲了使這些網絡數據具有更強的可移植性,使相同的代碼在大端和小端主機上都能正常運行,我們可以調用以下庫函數進行網絡字節序和主機字節序的相關轉換:

//主機字節序轉換爲網絡字節序
uint32_t htonl(uint32_t hostlong);//將32長整數從主機字節序轉換爲網絡字節序,
                                  //如果主機字節序是小端,則函數會做相應大小
                                  //端轉換後返回;如果主機字節序是大端,則函
                                  //數不做轉換,將參數原封不動返回。
uint16_t htons(uint16_t hostshort);

//網絡字節序轉換爲主機字節序
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

二、TCP網絡通信過程

1、三次握手過程

這裏寫圖片描述

2、通訊流程

這裏寫圖片描述

服務器:

首先調用socket()創建一個套接字用來通訊,其次調用bind()進行綁定這個文件描述符,並調用listen()用來監聽端口是否有客戶端請求來,如果有,就調用accept()進行連接,否則就繼續阻塞式等待直到有客戶端連接上來。連接建立後就可以進行通信了。

客戶端:

調用socket()分配一個用來通訊的端口,接着就調用connect()發出SYN請求並處於阻塞等待服務器應答狀態,服務器應答一個SYN-ACK分段,客戶端收到後從connect()返回,同時應答一個ACK分段,服務器收到後從accept()返回,連接建立成功。客戶端一般不調用bind()來綁定一個端口號,並不是不允許bind(),服務器也不是必須要bind()。

當客戶端沒有自己進行bind時,系統隨機分配給客戶端一個端口號,並且在分配的時候,操作系統會做到不與現有的端口號發生衝突。但如果自己進行bind,客戶端程序就很容易出現問題,假設在一個PC機上開啓多個客戶端進程,如果是用戶自己綁定了端口號,必然會造成端口衝突,影響通信。


三、代碼實現

1、函數介紹

int socket(int domain,int type,int protocol);
//domain:該參數一般被設置爲AF_INET,表示使用的是IPv4地址。還有更多選項可以利用man查看該函數
//type:該參數也有很多選項,例如SOCK_STREAM表示面向流的傳輸協議,SOCK_DGRAM表示數據報,我們這裏實現的是TCP,因此選用SOCK_STREAM,如果實現UDP可選SOCK_DGRAM
//protocol:協議類型,一般使用默認,設置爲0

該函數用於打開一個網絡通訊接口,出錯則返回-1,成功返回一個socket(文件描述符),應用進程就可以像讀寫文件一樣調用read/write在網絡上收發數據。

int bind(int sockfd,const struct sockaddr*addr,socklen_t addrlen);
//sockfd:服務器打開的sock
//後兩個參數可以參考第四部分的介紹

服務器所監聽的網絡地址和端口號一般是固定不變的,客戶端程序得知服務器程序的地址和端口號後就可以向服務器發起連接,因此服務器需要調用bind來綁定一個固定的網絡地址和端口號。bind成功返回0,出錯返回-1。
bind()的作用:將參數sockfd和addr綁定在一起,是sockfd這個用於網絡通訊的文件描述符監聽addr所描述的地址和端口號。

 int listen(int sockfd,int backlog);
//sockfd的含義與bind中的相同。
//backlog參數解釋爲內核爲次套接口排隊的最大數量,這個大小一般爲5~10,不宜太大(是爲了防止SYN攻擊)

該函數僅被服務器端使用,listen()聲明sockfd處於監聽狀態,並且最多允許有backlog個客戶端處於連接等待狀態,如果收到更多的連接請求就忽略。listen()成功返回0,失敗返回-1。

int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
//addrlen是一個傳入傳出型參數,傳入的是調用者的緩衝區cliaddr的長度,以避免緩衝區溢出問題;傳出的是客戶端地址結構體的實際長度(有可能沒有佔滿調用者提供的緩衝區)。如果給cliaddr參數傳NULL,表示不關心客戶端的地址。

典型的服務器程序是可以同時服務多個客戶端的,當有客戶端發起連接時,服務器就調用accept()返回並接收這個連接,如果有大量客戶端發起請求,服務器來不及處理,還沒有accept的客戶端就處於連接等待狀態。
三次握手完成後,服務器調用accept()接收連接,如果服務器調用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來。

int connect(int sockfd,const struct sockaddr* addr,socklen_t addrlen);

這個函數只需要有客戶端程序來調用,調用該函數後表明連接服務器,這裏的參數都是對方的地址。connect()成功返回0,出錯返回-1。

關於通用套接字地址結構struct sockaddr:
這個數據結構是通用的,由於使用的通訊協議族是不確定的,在傳參的時候會將特定的協議族地址數據結構指針(struct sockaddr*)強制轉爲通用的地址數據結構指針(struct sockaddr_in*),然後由內核根據傳入的第一個數據結構的第一個成員(sockaddr_family)來確定具體的結構類型。這也確保了不同的協議族地址數據結構可以使用同一個原型的函數。
套接字數據結構本身並不參與通信,僅在主機上使用。

2、服務器代碼

#include <stdio.h>
#include <stdlib.h>        //標準庫函數和宏
#include <errno.h>         //錯誤號定義和錯誤處理
#include <string.h>
#include <sys/socket.h>    //socket函數及數據結構
#include <sys/types.h>     //數據類型定義
#include <netinet/in.h>    //sockaddr_in結構定義
#include <arpa/inet.h>     //IP地址轉換函數
#inclued <unistd.h>        //POSIX操作系統API

#define PORT 1500       
#define BACKLOG 1       

int main()
{
    int sockfd, connectfd, addr_len;    //socket文件操作符
    struct sockaddr_in server;          //套接字地址數據結構
    struct sockaddr_in client;          

    sockfd = socket(AF_INET, SOCK_STREAM, 0);   //創建套接字(數據結構)
    if(-1 == sockfd)
    {
        perror("sockfd failed\n");
        exit(1);
    }

   //設置套接字地址數據結構
    bzero(&server, sizeof(server));  
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr.s_addr = htonl(INADDR_ANY);

    //綁定套接字數據結構(內核空間)和套接字地址數據結構(用戶空間)
    if(-1 == bind(sockfd, (struct sockaddr*)&server,sizeof(struct sockaddr)))
    {
        perror("bind failed\n");
        exit(0);
    }

    //監聽
    if(-1 == listen(sockfd, BACKLOG))
    {
        perror("liseten failed\n");
        exit(1);
    }

    //接收客戶端請求並提供服務
    addr_len = sizeof(struct sockaddr_in);
    connectfd = accept(sockfd, (struct sockaddr*)&client,&addr_len );

    if(-1 == connectfd)
    {
        perror("accept failed\n");
        exit(1);
    }

    printf("connect success\n");
    send(connectfd, "Welcome to my server!",22,0);

   // 關閉套接字
    close(connectfd);
    close(sockfd);

    return 0;
}

3、客戶端代碼

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <unistd.h>

#define PORT 1500
#define IP "xxx.xxx.xxx.xxx" //更改爲server的IP地址
#define MAXSIZE 100

int main()
{
    int sockfd, num;
    char buf[MAXSIZE];
    struct sockaddr_in server;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd)
    {
        perror("socket failed\n");
        exit(0);
    }

    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr.s_addr = inet_addr(IP);

    if(-1 == connect(sockfd, (struct sockaddr*)&server, sizeof(struct sockaddr)))
    {
        perror("connect failed\n");
        exit(-1);
    }

    printf("succsessful connection\n");

    num = recv(sockfd, buf, MAXSIZE, 0);
    printf("count: %d, recv: %s \n", num, buf)
    close(sockfd);

    return 0;
}

四、參考

從問題看本質:socket到底是什麼?
socket編程之實現一個簡單的TCP通信
Socket編程的頭文件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章