目錄
一、端口號
二、初識TCP/UDP協議
三、網絡字節序
3.1 概念
3.2 常用API
四、Socket套接字
4.1 概念
4.2 常用API
(1)socket
(2)bind
sockaddr結構
(3)listen
(4)accept
(5)connect
(6)recvfrom
(7)sendto
4.3 地址轉換函數
(1)inet_aton
(2)inet_addr
(3)inet_pton
一、端口號
網絡協議棧中的下三層,主要解決的是如何將數據安全可靠的送到遠端機器上的問題。
在上層,用戶使用特定的應用層軟件完成數據的發送和接收。而軟件在啟動后變為了進程,因此我們日常網絡通信的本質,就是進程間通信!
問題1:一個進程通過網絡將數據傳輸到遠端主機上時,如何區分要把數據傳給主機上的哪個進程呢?
實際上,每個進程都有屬于自己的端口號(port)。IP地址用于標識主機的唯一性,而端口號則用于標識一個進程在該主機中的唯一性。所以網絡通信中我們不止需要IP地址來找到目標主機,還需要端口號找到目標進程
端口號是傳輸層協議的內容,是一個2字節16位的整數,用于在主機中標識一個進程的唯一性。因此通過IP地址+端口號就能標識全網唯一的一個進程
問題2:進程PID也可以標識進程在主機中的唯一性,為什么還要有端口號?
- 不是所有的進程都需要進行網絡通信,但是所有進程都要有自己的PID
- 將系統和網絡功能解耦
一個端口號只能被一個進程綁定,但一個進程可以綁定多個端口號
二、初識TCP/UDP協議
傳輸層協議(TCP和UDP)的數據段中分別記錄了源端口號和目的端口號,用來描述數據是從哪個進程發的、要發給哪個進程
關于TCP和UDP協議,我們首先對它們有一個簡單且直觀的認識,后續再進行深入了解
TCP(Transmission Control Protocol,傳輸控制協議):
- 面向連接
- 保證數據傳輸可靠性
- 面向字節流
UDP(User Datagram Protocol,用戶數據報協議):
- 無連接
- 不保證數據傳輸可靠性
- 面向數據報
三、網絡字節序
3.1 概念
內存中的多字節數據相對于內存地址而言有大端和小端的區別,因此主機也分為大端機和小端機
讓我們回顧一下大端和小端的概念?
大端:數據的高位存儲在內存的低位
小端:數據的高位存儲在內存的高位
不止是內存,網絡數據流中同樣有大端小端之分。發送方在發送數據時通常將發送緩沖區中的數據按內存地址從低到高的順序發出,接收方將數據保存在接收緩沖區中,也是按內存地址從低到高的順序保存。
問題在于,不同類型的主機在跨網絡互相傳輸數據時就可能導致問題。例如大端機將數據發送給小端機,就可能導致數據的錯亂
因此TCP/IP協議規定,發送到網絡中的數據流應統一按照大端字節序發送。也就是說不論是大端機還是小端機,都要按照TCP/IP規定的網絡字節序來發送或接收數據
所以如果發送數據的主機是小端機,必須先將數據轉換成大端字節序后再發送。在后面調用套接字相關API時,我們也通常需要對端口號和ip地址進行網絡字節序轉換。
3.2 常用API
為了讓網絡程序具有可移植性,我們可以使用下列庫函數進行主機字節序和網絡字節序的轉換
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
其中h表示host,n表示network,l表示32位長整型,s表示16位短整型
例如htonl就是將32位長整型從主機字節序轉為網絡字節序,適用于轉換IP地址
如果主機字節序本身是小端,調用對應庫函數后則會將參數做相應大小端轉換后返回;如果主機字節序已經是大端了,則不作改變
四、Socket套接字
4.1 概念
套接字(Socket)是一種獨立于協議的網絡編程接口,是對網絡中不同主機的應用進程之間進行雙向通信的端點的抽象。套接字上聯應用進程,下接網絡協議棧,是應用程序與網絡協議棧進行交互的接口。
套接字包括?IP 地址和端口號兩個部分,可以用來區分不同的進程之間的數據傳輸。傳輸層使用的協議不同,套接字的種類也會發生相應的改變。
在Linux中,套接字的本質也是文件,因此有對應的網絡文件描述符,用戶通過網絡文件描述符對套接字進行操作。
4.2 常用API
(1)socket
#include <sys/types.h>
#include <sys/socket.h>int socket(int domain, int type, int protocol);
socket函數類似于打開文件的操作,會創建套接字并返回一個網絡文件描述符,其中:
- domain:協議域,又稱協議族,例如AF_INET代表IPv4協議,AF_INET6代表IPv6協議
- type:指定socket類型,例如流式套接字SOCK_STREAM(TCP)和數據報套接字SOCK_DGRAM(UDP)
- protocal:指定協議信息,常見的有IPPROTO_TCP、IPPROTO_UDP等,通常設置為0代表自動選擇套接字類型對應的默認協議
創建成功返回一個網絡文件描述符,失敗返回-1并設置環境變量errno
例如:
(2)bind
#include <sys/types.h>
#include <sys/socket.h>int bind(int socket, const struct sockaddr *address, socklen_t address_len);
bind函數用于將一個服務的ip地址和端口號綁定到一個套接字上,一般是服務端在綁定監聽套接字時會用到。客戶端則不必要調用bind綁定,因為客戶端的端口號由內核自動分配
其中:
- socket:待綁定的網絡文件描述符
- address:指向一個sockaddr結構體的指針,該結構體包含了要綁定的ip地址和端口號
- address_len:address指向的結構體大小
成功綁定返回0, 失敗返回-1并設置errno
例如:
uint16_t port = 8888; //端口號
string ip = "127.0.0.1"; //字符串格式的ip地址
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //創建套接字
if (sockfd < 0)
{// 創建套接字失敗時//...
}
//填充結構體字段
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; //IPv4協議
local.sin_port = htons(port);
inet_aton(ip.c_str(), &(local.sin_addr));
if (bind(sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) // 綁定
{//綁定失敗時//...
}
填充結構體字段時,需要對端口號進行網絡字節序轉換和對字符串格式的ip地址轉四字節ip地址后再填充到sockaddr_in結構體中
關于ip地址的格式轉換函數會在后面提及,這里先簡單提一下sockaddr的結構
sockaddr結構
關于socket的API是一層抽象的網絡編程接口,適用于各種底層網絡協議,如IPv4、IPv6等,但是各種網絡協議的地址格式并不相同。
例如IPv4的地址用sockaddr_in結構體表示,其中包含16位地址類型、16位端口號和32位ip地址
不同的結構體中,前16位都填充了ip地址的協議類型,因此我們可以統一用struct sockaddr*類型接收,取得結構體首地址后按位數獲取地址類型字段就可以確定是哪一種結構體了。
在使用Unix域套接字進行本機進程間通信時,綁定時就得使用sockaddr_un結構
(3)listen
#include <sys/types.h>
#include <sys/socket.h>int listen(int sockfd, int backlog);
listen函數常用于服務端監聽來自客戶端的TCP連接請求,通常在調用bind函數后使用,成功返回0,失敗返回-1并設置errno
其中:
- sockfd:將被設置為監聽狀態的網絡文件描述符
- backlog:設置全連接隊列的長度(全連接隊列用于臨時維護未被上層accept的已經建立好的連接,長度為backlog+1)
例如:
uint16_t port = 8888; //端口號
string ip = "127.0.0.1"; //字符串格式的ip地址
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //創建套接字
if (sockfd < 0)
{// 創建套接字失敗時//...
}
//填充結構體字段
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; //IPv4協議
local.sin_port = htons(port);
inet_aton(ip.c_str(), &(local.sin_addr));
if (bind(sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) // 綁定
{//綁定失敗時//...
}
if (listen(sockfd, 10) < 0) // 將套接字設置為監聽狀態,全連接隊列最多存放10+1個連接
{//監聽失敗時//...
}
(4)accept
#include <sys/types.h>
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函數常用于服務端從全連接隊列中接收來自客戶端的TCP連接請求并創建一個新的套接字,通常用于listen函數后。成功會返回該套接字的文件描述符用來負責后續的數據通信服務,失敗返回-1并設置errno。
如果全連接隊列中暫時沒有Tcp連接請求,accept函數將阻塞等待直到有客戶端發起連接請求(除非服務器處于非阻塞狀態)
其中:
- sockfd:被綁定并設置為監聽狀態的套接字對應的文件描述符
- addr:指向sockaddr結構體的指針,用于填充客戶端對應的地址信息。設置為NULL表示不關心客戶端地址
- addrlen:指向socklen_t的指針,表示addr的大小
例如:
uint16_t port = 8888; //端口號
string ip = "127.0.0.1"; //字符串格式的ip地址
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //創建套接字
if (sockfd < 0)
{// 創建套接字失敗時//...
}
//填充結構體字段
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; //IPv4協議
local.sin_port = htons(port);
inet_aton(ip.c_str(), &(local.sin_addr));
if (bind(sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) // 綁定
{// 綁定失敗時//...
}
if (listen(sockfd, 10) < 0) // 將套接字設置為監聽狀態,全連接隊列最多存放10+1個連接
{// 監聽失敗時//...
}
struct sockaddr_in client; // 存儲客戶端信息的結構體
socklen_t len = sizeof(client);
int newfd = accept(sockfd, (struct sockaddr *)&client, &len); // sockfd只負責獲取連接,newfd負責后續的數據通信服務
if (newfd < 0)
{// 接收失敗時//...
}
(5)connect
#include <sys/types.h>
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect函數常用于發起建立網絡連接的請求,成功返回0,失敗返回-1并設置errno
其中:
- sockfd:調用socket函數創建套接字成功后返回的文件描述符
- addr:指向sockaddr結構體的指針,其中包含了準備建立連接的目標服務器地址信息
- addrlen:addr指向的結構體的大小
例如:
string serverip = "127.0.0.1";
uint16_t serverport = 8888;
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 創建套接字
if (sockfd < 0)
{// 創建套接字失敗時//...
}
// 填充結構體字段
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
// 發起連接
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{// 連接發起失敗時//...
}
(6)recvfrom
#include <sys/types.h>
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
recvfrom常用于使用UDP協議(或其他無連接的數據報服務)時從套接字中讀取數據,成功返回讀取到的字節數,當套接字已經關閉時返回0,出錯返回-1并設置errno
其中:
- sockfd:已打開的套接字文件描述符
- buf:指向用于存放接收到的數據的緩沖區的指針
- len:緩沖區大小
- flags:控制接收行為的標志,通常設置為0表示阻塞模式
- src_addr:指向一個sockaddr結構體,存儲數據來源方的地址信息
- addrlen:代表sockaddr結構體的大小
例如:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 創建套接字
if (sockfd < 0)
{//...
}
char buffer[1024];
sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,(struct sockaddr *)&temp, &len); // 接收服務端返回的消息
//...
(7)sendto
#include <sys/types.h>
#include <sys/socket.h>ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
sendto函數常用于使用UDP協議時通過指定的socket將數據發送到目標主機,成功返回實際發送的字節數,失敗返回-1并設置errno
其中:
- sockfd:已打開的套接字文件描述符
- buf:指向要發送的數據
- len:要發送的數據長度
- flags:標志位,通常設置為0
- dest_addr:指向存儲目標主機地址信息的sockaddr結構體
- addrlen:結構體大小
4.3 地址轉換函數
sockaddr_in結構體中的成員sin_addr表示32位的ip地址,但我們日常中見到的ip地址通常是點分十進制格式的字符串表示的。通過一些函數可以實現ip地址在兩種格式間的轉換。
字符串轉32位ip地址:
(1)inet_aton
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int inet_aton(const char *cp, struct in_addr *inp);
其中:
- cp:待轉換的點分十進制ip地址字符串
- inp:指向in_addr結構體的指針,存儲轉換后的網絡字節序ip地址
in_addr內部存放了一個32位整型用于存儲轉換后的ip地址,其結構如下:
typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};
例如:
struct sockaddr_in addr;
inet_aton("127.0.0.1", &addr.sin_addr);
?
(2)inet_addr
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>in_addr_t inet_addr(const char *cp);
其中cp是待轉換的點分十進制ip地址字符串
例如:
struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
(3)inet_pton
#include <arpa/inet.h>int inet_pton(int af, const char *src, void *dst);
其中:
- af:協議族
- src:指向點分十進制ip地址字符串的指針
- dst:指向用于存儲轉換后ip地址的內存區域
網絡字節序ip地址轉點分十進制的函數有inet_ntoa、inet_ntop,有興趣的可以自行查閱文檔
完.