TCP/IP網絡編程筆記

一、基礎

1.1 套接字定義

套接字是網絡數(shù)據(jù)傳輸?shù)能浖O備。我們可以通過套接字完成數(shù)據(jù)傳輸。

1.2 相關函數(shù)

網絡編程中接受連接請求的套接字創(chuàng)建過程如下:

  1. 調用socket函數(shù)創(chuàng)建套接字
  2. 調用bind函數(shù)分配IP和端口號
  3. 調用listen函數(shù)轉換為可接受請求狀態(tài)
  4. 調用accept受理連接請求
  • Linux平臺相關函數(shù)
    #include <sys/socket.h>
    int socket(int domain, int type, int protocol);
         成功返回文件描述符,失敗返回-1
    int bind(int socketfd, struct sockadr* myaddr, socklen_t addrlen);
    int listen(int socketfd, int backlog);
    int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
    int connect(int sockdf, struct sockaddr* serv_addr, socklen_t addrlen);
         成功返回0,失敗返回-1
    
  • Windows平臺相關函數(shù)
    #include <winsock2.h>
    //需要鏈接ws2_32.lib
      //初始化
    int WSAStartup(WORD wVersionRequested,LPWSADATA lpwSAData);
            成功返回0,失敗返回錯誤代碼
            wVersionRequested    使用的Winsock版本信息
            lpwSAData                   WSADATA結構體變量的地址
    MAKEWORD(1,2);  //宏函數(shù),主版本為1,副版本2,返回0x0201
    //注銷
    int WSACleanup();
       成功返回0,失敗返回SOCKET_ERROR
    //套接字
    SOCKET socket(int af, int type, int protocol);
       成功時返回套接字句柄,失敗返回INVALID_SOCKET
    int bind(SOCKET s, const struct sockaddr* name, int namelen);
       成功返回0,失敗返回SOCKET_ERROR
    int  listen(SOCKET s, int backlog);
       成功返回0,失敗返回SOCKET_ERROR
    SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);
       成功時返回套接字句柄,失敗返回INVALID_SOCKET
    int connect(SOCKET s, const struct sockaddr* name, int namelen);
       成功返回0,失敗返回SOCKET_ERROR
    int closesocket(SOCKET s);
       成功返回0,失敗返回SOCKET_ERROR
    

1.3 類型和設置

1.3.1 協(xié)議(Protocol)

  • 協(xié)議族分類
    • PF_INET IPV4協(xié)議族
    • PF_INET6 IPV6協(xié)議族
    • PF_LOCAL 本地通信的Unix協(xié)議族
    • PF_PACKET 底層套接字的協(xié)議族
    • PF_IPX IPX Novell協(xié)議族
      通過套接字的第一個參數(shù)傳遞使用的協(xié)議分類信息,domain決定使用的protocol。
  • 套接字類型
    類型指的是套接字的數(shù)據(jù)傳輸方式,決定了協(xié)議族不能決定數(shù)據(jù)傳輸方式,如PF_INET可能存在多種數(shù)據(jù)傳輸方式。
    • SOCK_STREAM(面向連接的套接字)
      面向連接的套接字具有以下特點:

      • 傳輸過程中數(shù)據(jù)不會消失
      • 按序傳輸數(shù)據(jù)
      • 傳輸?shù)臄?shù)據(jù)不存在數(shù)據(jù)邊界

      收發(fā)數(shù)據(jù)的內部有緩沖(字節(jié)數(shù)組)。套接字傳輸?shù)臄?shù)據(jù)保存到該數(shù)組。在數(shù)組容量內,可能在充滿緩沖后一次讀取,也可能分多次讀取。如果緩沖被填滿,傳輸套接字將停止傳輸。

    • SOCK_DGRAM(面向消息的套接字)
      面向消息的套接字特點: 不可靠的、不按序傳遞、以數(shù)據(jù)的高速傳遞為目的的套接字。

1.4 地址族和字節(jié)序

1.4.1 地址族

IP是網絡協(xié)議的簡寫,是為了收發(fā)網絡數(shù)據(jù)而分配給計算機的值。只要有IP就能像數(shù)據(jù)發(fā)送到目標計算機,但無法發(fā)送給最終的應用程序。
端口號是為了區(qū)分程序中創(chuàng)建的套接字而分配給套接字的序號。NIC通過IP向計算機內部發(fā)生數(shù)據(jù),操作系統(tǒng)利用端口號將數(shù)據(jù)分配給套接字。
IP地址分為IPV4(4字節(jié))和IPV6(16字節(jié))兩類。IPV6是為了解決201年前后IP地址耗盡而提出的標準。IPV4分為A、B、C、D四類,由網絡ID和主機主機ID構成。 IP網絡數(shù)據(jù)傳輸時,先基于網絡地址(網絡ID)把數(shù)據(jù)發(fā)送到網絡。網絡收到數(shù)據(jù)后,根據(jù)主機ID將數(shù)據(jù)發(fā)送到目標計算機。
地址分類

  • A類地址首字節(jié) 0-127,首位以0開始
  • B類地址首字節(jié)128-191,首位以10開始
  • C類地址首字節(jié)192-223,首位以110開始

端口號

  • 端口號由16位構成,可分配端口號為0-65535
  • 知名端口號:0-1023。一般分配給特定程序。

1.4.2 地址信息表示

struct sockaddr_in
{
    sa_family_t       sin_family;  //地址族
    uint16_t          sin_port;     //16位端口號
    struct in_addr    sin_addr;  //32位IP地址
    char              sin_zero[8];  //不使用
}

in_addr結構體聲明為uint32_t,只需要當作32位整數(shù)。
sin_zero是使結構體與socd_addr保持一致而插入的成員。
之所以使用sockaddr_in,而不直接使用sockaddr,原因在于sockaddr結構體,如下:

struct sock_addr
{
    sa_family_t sin_family;
    char sa_data[14];
}

sa_data中包含地址和端口號,其余部分為0。直接填充這些信息會比較麻煩,于是有了sockaddr_in,最后轉換為sockaddr型結構體。

1.4.3 字節(jié)序

大端序: 高位字節(jié)放在地位地址
小端序: 高位字節(jié)放在高位地址
在通過網絡傳輸數(shù)據(jù)時約定統(tǒng)一方式,即網絡字節(jié)序。網絡字節(jié)序是大端序
字節(jié)序轉換

unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
//h--->host字節(jié)序
//n-->network字節(jié)序
//s-->2字節(jié)short,常用于端口號轉換
//l-->4字節(jié),常用于IP轉換

地址字符串的字節(jié)序轉換

#include <arpa/inet.h>

in_addr_t inet_addr(const char* string);  //點分十進制字符串---> 大端序整數(shù)值

int inet_aton(const char* string,  struct in_addr* addr);  //string轉換結果保存到addr地址值中

char* inet_ntoa(struct in_addr adr);  //轉換為字符串形式。長期保存時,需要對char*內容進行復制

網絡地址初始化

struct sockaddr_in addr;
char* server_ip = "211.217.168.11"; //ip字符串
char* serv_port = "9190";  //端口號

memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(server_ip);
addr.sin_port = htons(server_port);

INADDR_ANY
利用INADDR_ANY分配Ip地址,將自動獲取計算機的地址,若計算機有多個IP地址,則可以從不同IP、相同端口號中接收數(shù)據(jù)。服務器一般采用這種方式。

1.5 域名和網絡地址

將容易記、易表達的域名分配并取代IP地址。域名是賦予服務器端的虛擬地址,而非實際地址。通過DNS請求轉換地址。若DNS服務器無法解析,會詢問其他DNS服務器,并提供給用戶。

//
#include <netdb.h>

struct hostent* gethostbyname(const char* hostname);

structhostent* gethostbyaddr(const char* addr, socklen_t len, int family);

1.6套接字 多種可選項

1.6.1 可選項

套接字可選項是分層的。IPPROTO_IP是IP協(xié)議相關選項,IPPROTO_TCP是tcp協(xié)議相關可選項,SOL_SOCKET是套接字相關通用可選項。
設置可選項

#include <sys/socket.h>

int getsockopt(int sock, int level, int optname, void* optval, socklen_t* optlen);
     sock 套接字文件描述符
     level 協(xié)議層
     optname 可選項名字
     optval    保持查看結果的緩沖地址
     optlen   緩沖大小

int setsockopt(int sock, int level, int optname, const void* optval, socklen_t optlen);

SO_TYPE 查看套接字類型
SO_SNDBUF/SO_RCVBUF 輸入輸出緩沖區(qū)大小
SO_REUSEADDR :默認為0(假),即無法分配Time-wait狀態(tài)下的套接字端口號, 為1(真)可將Time-wait狀態(tài)下的套接字端口號重新分配給新的套接字
套接字四次揮手后并非立即清除,二十經過一段時間的Time-wait狀態(tài)。先斷開連接(發(fā)送FIN消息)的主機才經過Time-wait狀態(tài)。因此服務器先斷開連接,無法立即重新運行。
TCP_NODEALY:設置為1時,禁用Nagle算法。
tcp套接字默認使用Nagle算法(只有收到前一數(shù)據(jù)的ACK時,才發(fā)送下一數(shù)據(jù))交換數(shù)據(jù),因此最大限度的進行緩沖,直到收到ACK。為了提高網絡傳輸效率,必須使用Nagle算法。但在網絡流量未受到太大影響時,不使用Nagle算法會更快。如大文件數(shù)據(jù)。

1.7 進程

1.8 IO復用與IO函數(shù)

無論連接多少客戶端,提供服務的進程只有一個。select是最具代表性的服務端復用方法。

1.9 線程

1.10 信號處理

二、TCP

2.1 定義

IP層解決數(shù)據(jù)傳輸中的路徑選擇問題。TCP和UDP利用IP層提供的路徑信息完成實際的數(shù)據(jù)傳輸。

2.2 服務端

TCP服務端函數(shù)調用通常遵循如下順序:

  1. socket -- 創(chuàng)建套接字
  2. bind -- 分配套接字地址
  3. listen -- 等待連接請求狀態(tài)
  4. accept -- 允許連接
  5. read/wrote -- 數(shù)據(jù)交換
  6. close -- 斷開連接

listen后,若有新的連接請求,應該按序處理。受理請求意味著進入可接受數(shù)據(jù)的狀態(tài)。
accept受理連接請求。其內部產生用于數(shù)據(jù)IO的套接字,并返回其文件描述符。
客戶端通過connect發(fā)起連接請求,當服務端接收連接請求或者發(fā)生異常中斷連接請求時返回。服務端接收連接請求只是將請求記錄加入等待隊列,并不表示調用accept,不表示立即進行數(shù)據(jù)交換。

2.3 迭代服務端

通過插入循環(huán)語句,反復調用accept函數(shù)來持續(xù)受理客戶端連接請求的方式。

2.4 迭代回聲服務/客戶端

TCP數(shù)據(jù)傳輸不存在邊界。服務端通過1次write函數(shù)傳輸數(shù)據(jù),但是如果數(shù)據(jù)太大,可能會分成多個數(shù)據(jù)包發(fā)送。另外,客戶端也可以未完全收到數(shù)據(jù)就read。解決方法是提前確定接收數(shù)據(jù)的大小。在無法預知數(shù)據(jù)大小的情況下,需要應用層協(xié)議的定義。收發(fā)數(shù)據(jù)的過程中定義號規(guī)則以表示數(shù)據(jù)的邊界。

2.5 半關閉

IO緩沖特性:

  • I/O緩沖在每個套接字中單獨存在
  • 在創(chuàng)建套接字時自動生成
  • 關閉套接字后,也會繼續(xù)傳輸輸出緩沖中的遺留數(shù)據(jù)
  • 關閉套接字,會丟失輸入緩沖中的數(shù)據(jù)

Linux的close和Windows的closescket意味著完全斷開連接,即無法傳輸和接受數(shù)據(jù)。這樣可能存在主機A發(fā)送數(shù)據(jù)后斷開連接,無法繼續(xù)接收B的數(shù)據(jù)。因此,Half-close產生,即只關閉1個流。

半關閉函數(shù)

#include <sys/socket.h>

int shutdown(int sock, int howto);
     //howto為斷開方式信息

斷開方式如下:

  • SHUT_RD 斷開輸入流
  • SHUT_WR 斷開輸出流
  • SHUT_RWDR 同時斷開輸入輸出流

shutdown進行半關閉,同時發(fā)送EOF。

2.6 并發(fā)服務器

2.7 IO分離

2.8 select服務端

2.8.1 select調用過程

  1. 設置文件描述符、指定監(jiān)視范圍、設置超時
  2. 調用select函數(shù)
  3. 查看調用結果

2.9 多線程服務端

三、 UDP

3.1 定義

UDP提供不可靠數(shù)據(jù)傳輸。UDP不提供流控制機制。根據(jù)端口號將傳到主機的數(shù)據(jù)交付給最終的UDP套接字。
TCP比UDP慢主要由于:

  • 收發(fā)數(shù)據(jù)前后進行的連接設置和清楚操作
  • 收發(fā)數(shù)據(jù)過程中為保證可靠性而添加的流控制

3.2 UDP服務端/客戶端

3.2.1 UDP數(shù)據(jù)IO函數(shù)

#include <sys/cosket.h>

ssize_t sendto(int sock, void* buf, size_t bnytes, int flags,  struct sockaddr* to, socklen_t addrlen)

ssize_t recvfrom(int sock, void* buf, size_t nbytes, int flags, struct sockaddr* from, socklen_t* addrlen)

3.2.2 套接字地址分配

sendto前完成地址分配。如果sendto時未分配地址信息,首次調用時自動分配IP和端口號,分配結果保留到程序結束。IP為主機IP,端口號為任意未分配端口。
此外,也可以用bind分配ip和端口號。

3.3 數(shù)據(jù)傳輸特性和connect

存在數(shù)據(jù)邊界。通過snedto傳輸數(shù)據(jù)過程如下:

  1. 向套接字注冊目標IP和端口號
  2. 傳輸數(shù)據(jù)
  3. 刪除注冊信息
    未注冊目標信息的套接字稱為未連接套接字。反之稱為connected套接字。
    向同一目標主機進行長時間通信時,將UDP套接字變?yōu)閏onnected套接字會提高效率。傳輸過程中,1和3占傳輸?shù)?/3時間。

3.4 多播與廣播

四、Windows平臺

4.1 異步IO

4.2 重疊IO

4.3 IOCP

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容