相關結構體與函數
sockaddr、sockaddr_in結構體
sockaddr和sockaddr_in詳解
struct sockaddr
共16字節,協議族(family)占2字節,IP地址和端口號在sa_data字符數組中
/* Structure describing a generic socket address. */
struct sockaddr
{__SOCKADDR_COMMON(sa_); /* Common data: address family and length. */char sa_data[14]; /* Address data. */
};#define __SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family
struct sockaddr_in
更細致地劃分了協議族、端口號和IP地址,其中IP地址定義了新的結構體struct in_addr
,該結構體中宏定義了uint32_t
類型的變量,sin_zero字符數組存在的意義是為了使struct sockaddr_in
和struct sockaddr
大小相等,便于進行強制類型轉換(與bind()
等函數的參數有關)
/* Structure describing an Internet socket address. */
struct sockaddr_in
{__SOCKADDR_COMMON(sin_);in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. *//* Pad to size of `struct sockaddr'. */unsigned char sin_zero[sizeof(struct sockaddr) -__SOCKADDR_COMMON_SIZE -sizeof(in_port_t) -sizeof(struct in_addr)];
};/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};
總結
- 二者長度一樣,都是16個字節,即占用的內存大小是一致的,因此可以互相轉化。二者是并列結構,指向sockaddr_in結構的指針也可以指向sockaddr
- sockaddr常用于bind、connect、recvfrom、sendto等函數的參數,指明地址信息,是一種通用的套接字地址
- sockaddr_in 是internet環境下套接字的地址形式
- 在網絡編程中我們會對sockaddr_in結構體進行操作,使用sockaddr_in來建立所需的信息,最后使用類型轉化
- 一般先把sockaddr_in變量賦值后,強制類型轉換后傳入用sockaddr做參數的函數:sockaddr_in用于socket定義和賦值;sockaddr用于函數參數
socket()函數
socket函數用于創建一個新的socket,也就是向系統申清一個socket資源
socket函數用戶客戶端和服務端
int socket(int domain, int type, int protocol);
domain:協議域,又稱協議族(family)。常用的協議族有AF INET、AF INET6、AF LOCAL(或稱AF UNIX,Unix域Socket)、AF ROUTE等。協議族決定了socket的地址類型,在通信中必須采用對應的地址,如AF INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址
type:指定socket類型。常用的socket類型有SOCK_STREAM、SOCK_DGRAM、SOCK RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一種面向連接的socket,針對于面向連接的TCP服務應用。數據報式socket(SOCK_DGRAM)是一種無連接的socket,對應于無連接的UDP服務應用
protocol:指定協議。常用協議有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議
💡 第一個參數只能填AF INET,第二個參數只能填SOCK STREAM,第三個參數只填0
除非系統資料耗盡,socket函數一般不會返回失敗
返回值:成功則返回一個socket,失敗返回-1,錯誤原因存于errno中
💡 “資源耗盡”即Linux對打開文件數的限制,見2022-06-10筆記
// 第1步:創建服務端的socket
int listenfd;
for (int i = 0; i < 2000; ++i)
{if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){perror("socket");return -1;}cout << "sock id:" << listenfd << endl;
}
sock id:1012
sock id:1013
sock id:1014
sock id:1015
sock id:1016
sock id:1017
sock id:1018
sock id:1019
sock id:1020
sock id:1021
sock id:1022
sock id:1023
socket: Too many open files
inet_addr() 和inet_ntoa()函數
使用socket進行通信的時候,我們需要指定三個元素:通信域(地址族)、IP地址、端口號,這三個元素由SOCKADDR_IN結構體定義
為了簡化編程一般將IP地址設置為INADDR_ANY,如果需要使用特定的IP地址則需要使用inet_addr()
** 和**inet_ntoa()
函數
inet_addr()
和inet_ntoa()
完成字符串和in_addr結構體的互換
inet_addr()
函數參數cp代表點分十進制的IP地址,如1.2.3.4,返回值為in_addr_t
類型
/* Convert Internet host address from numbers-and-dots notation in CPinto binary data in network byte order. */
extern in_addr_t inet_addr (const char *__cp) __THROW;
inet_ntoa()
函數輸入為in_addr結構體而輸出為字符串
/* Convert Internet number in IN to ASCII representation. The return valueis a pointer to an internal array containing the string. */
extern char *inet_ntoa (struct in_addr __in) __THROW;
hostent結構體
hostent實例詳解
/* Description of data base entry for a single host. */
struct hostent
{char *h_name; /* Official name of host. */char **h_aliases; /* Alias list. */int h_addrtype; /* Host address type. */int h_length; /* Length of address. */char **h_addr_list; /* List of addresses from name server. */
#ifdef __USE_MISC
# define h_addr h_addr_list[0] /* Address, for backward compatibility.*/
#endif
};
struct hostent{char *h_name; //正式主機名char **h_aliases; //主機別名int h_addrtype; //主機IP地址類型:IPV4-AF_INETint h_length; //主機IP地址字節長度,對于IPv4是四字節,即32位char **h_addr_list; //主機的IP地址列表};#define h_addr h_addr_list[0] //保存的是IP地址
- h_name:官方域名(Official domain name)。官方域名代表某一主頁,但實際上一些著名公司的域名并未用官方域名注冊
- h_aliases:別名,可以通過多個域名訪問同一主機。同一 IP 地址可以綁定多個域名,因此除了當前域名還可以指定其他域名
- h_addrtype:gethostbyname() 不僅支持 IPv4,還支持 IPv6,可以通過此成員獲取IP地址的地址族(地址類型)信息,IPv4 對應 AF_INET,IPv6 對應 AF_INET6
- h_length:保存IP地址長度。IPv4 的長度為 4 個字節,IPv6 的長度為 16 個字節
- h_addr_list:這是最重要的成員。通過該成員以整數形式保存域名對應的 IP 地址。對于用戶較多的服務器,可能會分配多個 IP 地址給同一域名,
利用多個服務器進行均衡負載
在實際的應用中,一臺服務器往往有好幾個IP地址,而域名只有一個
,這樣設計的好處是,可以使系統分布設計,提升服務器的穩定性和抗災難能力
一般對服務器的訪問,則是先經過DNS(Domain Name System)
服務器,DNS通過均衡設計,返回合適的IP與客戶端進行交互,避免客戶端只連接一個IP,導致網絡擁堵
gethostbyname()函數
gethostbyname()函數:通過域名獲取IP地址
gethostbyname()函數詳解
客戶端中直接使用IP地址會有很大的弊端,一旦IP地址變化(IP地址會經常變動),客戶端軟件就會出現錯誤
而使用域名會方便很多,注冊后的域名只要每年續費就永遠屬于自己的,更換IP地址時修改域名解析即可,不會影響軟件的正常使用
域名僅僅是 IP 地址的一個助記符,目的是方便記憶,通過域名并不能找到目標計算機,通信之前必須要將域名轉換成 IP 地址
gethostbyname() 函數可以完成這種轉換
/* Return entry from host data base for host with NAME.This function is a possible cancellation point and therefore notmarked with __THROW. */
extern struct hostent *gethostbyname (const char *__name);
bind()函數
服務端用于將把用于通信的地址和端口綁定到socket上
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockfd:需要綁定的socket
addr:存放了服務端用于通信的地址和端口
addrlen:表示addr結構體的大小
返回值:成功則返回0,失敗返回-1,錯誤原因存于errno中
如果綁定的地址錯誤,或端口已被占用,bind函數一定會報錯,否則一般不會返回錯誤
💡 注意第二個參數為sockaddr
結構體指針
listen()函數
listen函數使用主動連接套接口變為被連接套接口,使得一個進程可以接受其它進程的請求,從而成為一個服務器進程
在TCP服務器編程中listen函數把進程變為一個服務器,并指定相應的套接字變為被動連接
/* Prepare to accept connections on socket FD.N connection requests will be queued before further requests are refused.Returns 0 on success, -1 for errors. */
extern int listen (int __fd, int __n) __THROW;
_fd:服務端的socket,標識綁定的,未連接的套接字的描述符
-n:掛起的連接隊列的最大長度
- 比如有100個用戶鏈接請求,但是系統一次只能處理20個,那么剩下的80個不能不理人家,所以系統就創建個隊列記錄這些暫時不能處理,一會兒處理的連接請求,依先后順序處理,那這個隊列到底多大?就是這個參數設置,比如2,那么就允許兩個新鏈接排隊。這個不能無限大,那內存就不夠了
- 可以手動設置這個參數,但是別太大
- 我們一般填寫這個參數為
SOMAXCONN
,讓系統自動選擇最合適的個數
connect()函數
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);參數:
第一個參數:int sockdf:socket文件描述符
第二個參數: const struct sockaddr *addr:傳入參數,指定服務器端地址信息,含IP地址和端口號
第三個參數:socklen_t addrlen:傳入參數,傳入sizeof(addr)大小
返回值:成功: 0失敗:-1,設置errno
當客戶端調用 connect()函數之后,發生一下情況之一才會返回(完成函數調用)
- 服務器端接收連接請求
- 發生斷網的異常情況而終端連接請求
需要注意的是,所謂的“接收連接”并不意味著服務器調用 accept()函數,其實是服務器端把連接請求信息記錄到等待隊列,因此 connect()函數返回后并不進行數據交換,而是要等服務器端 accept 之后才能進行數據交換(read、write)
客戶端端需要調用connect()連接服務器,connect和bind的參數形式一致,區別在于bind的參數是自己的地址,而connect的參數是對方的地址
accept()函數
socket的accept函數解析
socket中accept()函數的理解
/* Await a connection on socket FD.When a connection arrives, open a new socket to communicate with it,set *ADDR (which is *ADDR_LEN bytes long) to the address of the connectingpeer and *ADDR_LEN to the address's actual length, and return thenew socket's descriptor, or -1 for errors.This function is a cancellation point and therefore not marked with__THROW. */
extern int accept (int __fd, __SOCKADDR_ARG __addr,socklen_t *__restrict __addr_len);
send()函數
send函數用于把數據通過socket發送給對端
不論是客戶端還是服務端,應用程序都用send函數來向TCP連接的另一端發送數據
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- sockfd為已建立好連接的socket
- buf為需要發送的數據的內存地址,可以是C語言基本數據類型變量的地址,也可以數組、結構體、字符串,內存中有什么就發送什么
- len需要發送的數據的長度,為buf中有效數據的長度
- flags填0,其他數值意義不大
- 函數返回已發送的字符數,出錯時返回-1,錯誤信息errno被標記
- 注意,就算是網絡斷開,或socket已被對端關閉,send函數不會立即報錯,要過幾秒才會報錯
- 如果send函數返回的錯誤(<=0),表示通信鏈路已不可用
recv()函數
recv函數用于接收對端通過socket發送過來的數據
不論是客戶端還是服務端,應用程序都用recv函數接收來自TCP連接的另一端發送過來數據
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
參數信息同send()函數
函數返回已接收的字符數,出錯時返回-1,失敗時不會設置errno的值
- 如果socket的對端沒有發送數據,recv函數就會等待
- 如果對端發送了數據,函數返回接收到的字符數
- 出錯時返回-1,如果socket被對端關閉,返回值為0
- 如果recv函數返回的錯誤(<=0),表示通信通道已不可用
💡 數據收發時的數據量會受到發送緩沖區和接收緩沖區大小的限制
數據收發時要注意字節序的問題(不同主機字節序的問題)