目錄
1. 套接字
1.1 在Linux平臺下構建套接字
1.1.1 用于接聽的套接字(服務器端套接字)
1.1.2 用于發送請求的套接字(客戶端套接字)
1.2 在Windows平臺下構建套接字
1.2.1 Winsock的初始化
1.2.2 用于接聽的套接字(服務器端套接字)
1.2.3 用于發送請求的套接字(客戶端套接字)
1.3 Linux和Windows套接字的區別
1.4 套接字特性
1.4.1 socket函數
1.4.1.1 協議族信息(domain形參)
1.4.1.2? 套接字類型(Type形參)
1.4.1.2.1 面向連接的套接字(SOCK_STREAM)
1.4.1.2.2 面向消息的套接字(SOCK_DGRAM)
1.4.1.3 協議的最終選擇(protocol形參)
2. 地址族和數據序列
2.1 網絡地址
2.1.1 IPv4(常用)
2.1.1.1 網絡地址和主機地址
2.1.2 端口號
2.1.3 bind函數
2.1.3.1 sockaddr_in結構體
2.1.4 網絡字節序
2.1.5 字節序轉換
2.1.6 網絡地址的初始化和分配
2.1.6.1 inet_addr函數
2.1.6.2 inet_aton函數(Windows不存在此函數)
2.1.6.3 inet_ntoa函數(和inet_aton函數相反)
2.1.6.4 INADDR_ANY常數
2.1.6.5 WSAStringToAddress函數(只有Windows平臺有,不利于兼容性)
2.1.6.6 WSAAddressToString函數(只有Windows平臺有,不利于兼容性)
2.1.6.7 服務器端初始化IP地址時非常明確,為什么還要進行IP的初始化呢?
3.其余流程函數
3.1 進入連接等待狀態(listen函數)
3.2 受理客戶端連接請求(accpet函數)
3.3 客戶端請求連接(connect函數)
4. 基于TCP的服務器端/客戶端函數調用關系
網絡編程:編寫程序使兩臺連網的計算機進行數據交換。
1. 套接字
操作系統提供名為套接字的部件,套接字是網絡數據傳輸用的軟件設備,它能夠連接到因特網上,與遠程計算機進行數據傳輸。
1.1 在Linux平臺下構建套接字
對于Linux而言,socket操作與文件操作沒有區別。如:close函數不僅可以關閉文件也可以關閉套接字。
1.1.1 用于接聽的套接字(服務器端套接字)
第一步:調用socket函數創建套接字
#include<sts/socket.h>
int socket(int domain,int type,int protocol);
成功返回文件描述符,失敗返回-1
第二步:調用bind函數分配IP地址和端口號
#include<sts/socket.h>
int bind(int sockfd,struct sockaddr* myaddr,socklen_t addrlen);
成功返回0,失敗返回-1
第三步:調用listen函數轉為可接收請求狀態
#include<sts/socket.h>
int listen(int sockfd,int backlog);
成功返回0,失敗返回-1
第四步:調用accept函數受理連接請求
#include<sts/socket.h>
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
成功返回文件描述符,失敗返回-1
accpet函數一直等待,直到有連接請求時,才會有返回值。
第五步:調用write函數發送數據
#include<unistd.h>ssize_t write(int fd,const void* buf,size_t nbytes);
成功則返回寫入的字節數,失敗返回-1
fd:文件描述符參數
buf:保存要傳輸數據的緩沖地址值
nbytes:要傳輸數據的字節數
第六步:關閉套接字
#include<unistd.h>int close(int fd);
成功返回0,失敗返回-1
fd:文件描述符參數。?
close函數不僅可以關閉文件也可以關閉套接字。 客戶端調用close會向服務端的客戶端套接字文件描述符傳遞EOF。
1.1.2 用于發送請求的套接字(客戶端套接字)
第一步:調用socket函數創建套接字
如上。
第二步:調用connect函數連接服務器端
#include<sts/socket.h>
int connect(int sockfd,struct sockaddr* serv_addr,socklen_t addrlen);
成功返回0,失敗返回-1
第三步:調用read函數讀取服務器傳輸的信息
#include<unistd.h>ssize_t read(int fd,void *buf,size_t nbytes);
成功則返回接收的字節數(但遇到文件結尾則返回0),失敗返回-1
fd:數據接收對象的文件描述符參數
buf:保存接收數據的緩沖地址值
nbytes:要接收數據的字節數
第四步:關閉套接字
如上。
1.2 在Windows平臺下構建套接字
在Windows平臺下構建套接字要先進行Winsock的初始化。
1.2.1 Winsock的初始化
初始化版本庫
#include<winsock2.h>int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
成功返回0,失敗返回非0的錯誤代碼值
wVersionRequested:WORD是通過typedef定義的unsigned short類型,這個參數是提供套接字版本信息。可借助MAKEWORD宏函數來構建版本信息,如:
MAKEWORD(1,2); //主版本為1,副版本為2,返回0x0201
MAKEWORD(2,2); //主版本為2,副版本為2,返回0x0202
lpWSAData:傳入WSADATA型結構體變量地址(LPWSADATA是WSADATA的指針類型),調用完函數后會將相關參數,填充到這個參數里。
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
{......
}
注銷版本庫
#include<winsock2.h>int WSACleanup(void);
成功返回0,失敗返回SOCKET_ERROR
銷毀Winsock相關庫,無法再調用Winsock相關函數。
1.2.2 用于接聽的套接字(服務器端套接字)
?第一步:調用socket函數創建套接字
#include <winsock2.h>
SOCKET socket(int af,int type,int protocol);
成功返回套接字句柄,失敗返回INVALID_SOCKET
第二步:調用bind函數分配IP地址和端口號
#include <winsock2.h>
int bind(SOCKET s,const struct sockaddr* name,int namelen);
成功返回0,失敗返回SOCKET_ERROR
第三步:調用listen函數轉為可接收請求狀態
#include <winsock2.h>
int listen(SOCKET s,int backlog);
成功返回0,失敗返回SOCKET_ERROR
第四步:調用accept函數受理連接請求
#include <winsock2.h>
SOCKET accept(SOCKET s,struct sockaddr* addr,int* addrlen);
成功返回文件描述符,失敗返回INVALID_SOCKET
accpet函數一直等待,直到有連接請求時,才會有返回值。
第五步:調用send函數發送數據
#include<winsock2.h>
int send(SOCKET s,const char* buf,int len,int flags);
成功返回傳輸字節數,失敗返回SOCKET_ERROR
?s:表示數據傳輸對象連接的套接字句柄值
buf: 保存待傳輸數據的緩沖地址值
len:要傳輸的字節數
flags:傳輸數據時用到的多種選項信息
第六步:關閉套接字
#include <winsock2.h>
int closeconnect(SOCKET s);
成功返回0,失敗返回SOCKET_ERROR
1.2.3 用于發送請求的套接字(客戶端套接字)
第一步:調用socket函數創建套接字
如上。
第二步:調用connect函數連接服務器端
#include <winsock2.h>
int connect(SOCKET s,const struct sockaddr* name,int namelen);
成功返回0,失敗返回SOCKET_ERROR
第三步:調用recv函數,接收服務器端傳來的數據
#include <winsock2.h>
int recv(SOCKET s,const char* buf,int len,int flags);
成功返回接收的字節數(收到EOF時為0),失敗返回SOCKET_ERROR
?s:表示數據接收對象連接的套接字句柄值
buf: 保存接收數據的緩沖地址值
len:要接收的最大字節數
flags:接收數據時用到的多種選項信息
第四步:關閉套接字
如上。
1.3 Linux和Windows套接字的區別
-
文件描述符和句柄的區別:
在Linux中,文件描述符是不區分文件和套接字的,即兩個都是一樣的
在Windows中,句柄是區分文件和套接字的,并不完全一樣。
比較兩個系統的socket、listen和accept函數,可以發現,其實Linux的int sockfd就對應于Windows的SOCKET s,即SOCKET這個類型,就存有套接字句柄整形值,也類似于一種編號。
-
write和send的區別:
???????? 在Linux中,有write也有send函數,來傳輸數據。
??????? 在windows中,send函數只是比Linux中的write函數多了最后的flag參數。
1.4 套接字特性
1.4.1 socket函數
int socket(
int domain, //套接字中使用的協議族(Protocol Family)信息
int type, //套接字數據傳輸類型信息
int protocol //計算機間通信中使用的協議信息
);
成功返回文件描述符,失敗返回-1
?一個socket套接字=協議族+套接字類型+最終協議。
1.4.1.1 協議族信息(domain形參)
協議族:套接字通信中協議的分類。
名稱 | 協議族 |
PF_INET(常用) | IPv4互聯網協議族 |
PF_INET6 | IPv6互聯網協議族 |
PF_LOCAL | 本地通信的UNIX協議族 |
PF_PACKET | 底層套接字的協議族 |
PF_IPX | IPX Novell協議族 |
1.4.1.2? 套接字類型(Type形參)
套接字類型:套接字的數據傳輸方式。
1.4.1.2.1 面向連接的套接字(SOCK_STREAM)
特點:
- 傳輸過程中數據不會丟失
- 按序傳輸數據
- 傳輸的數據不存在數據邊界
- 套接字連接必須一一對應(一個客戶端套接字對應服務器端的一個套接字,n個對應n個套接字)
總結:可靠的、按序傳遞的、基于字節的面向連接的數據傳輸方式的套接字。注意接收和發送數據大小要相等。
特點:傳輸過程中數據不會丟失、傳輸的數據不存在數據邊界,解釋:
??????? 在接收的套接字內部,有一個由字節數組組成的緩沖區,從傳輸端傳過來的數據會先存儲到這個緩沖區里,如果緩沖區滿了,那么傳輸端就會停止傳輸,等待緩沖區中的數據被讀取完,再繼續傳輸。其中傳輸出錯,也會進行重傳服務,除特殊情況外,不會有數據丟失。
1.4.1.2.2 面向消息的套接字(SOCK_DGRAM)
特點:
- 快速傳輸
- 傳輸的數據可能丟失、損毀
- 傳輸的數據有數據邊界
- 限制每次傳輸的大小
總結:不可靠的,不按序傳遞的、以數據的高速傳輸為目的的套接字,不存在連接的概念。注意接收和發送數據次數要相等。
特點:傳輸的數據具有數據邊界,解釋:
??????? 每次傳輸都有大小限制,如果超過了這個限制,那么就得分批發送,即意味著接收數據的次數應和傳輸次數相同。而面向連接的套接字,沒有這個要求。
1.4.1.3 協議的最終選擇(protocol形參)
第三個參數的意義:同一協議族中存在多個數據傳輸方式相同的協議。
與套接字類型對應的:
- 面向連接的套接字:TCP套接字(IPPROTO_TCP),注意接收和發送數據大小要相等。
- 面向消息的套接字:UDP套接字(IPPROTO_UDP),注意接收和發送數據次數要相等。
2. 地址族和數據序列
IP(Internet Protocol網絡協議):為收發網絡數據而分配給計算機的值。
端口號:區分程序中創建的套接字而分配給套接字的序號。
2.1 網絡地址
分為兩類:IPv4(4字節地址族)、IPv6(16字節地址族)。
2.1.1 IPv4(常用)
IPv4標準的4字節IP地址,由網絡地址和主機地址組成。
2.1.1.1 網絡地址和主機地址
IPv4分為如下A、B、C、D四種類型:
通過首字節可以判斷其屬于哪種類型:
首字節范圍 | 類型 |
0~127 | A |
128~191 | B |
192~223 | C |
向對應IP地址主機傳輸數據,是先通過網絡地址,查找到對應的路由器或交換機,再由路由器或交換機,根據主機ID將數據分發到主機上。如圖:將數據發送到203.211.172.103上,會先找到網絡地址為203.211.172的路由器,路由器再通過主機ID:103將數據傳輸給對應主機。
2.1.2 端口號
端口號由16位構成,可分配端口號范圍為0~65535,但0~1023是知名端口號,會分配給特定應用程序,所以應當分配此范圍之外的值。另外,雖然端口號不能重復,但TCP套接字和UDP套接字不會共用端口號,所以允許重復。例如:某TCP套接字用了9130端口,則其余TCP套接字不能使用此端口,但UDP套接字可以使用9130端口。
2.1.3 bind函數
#include<sts/socket.h>
int bind(int sockfd,struct sockaddr* myaddr,socklen_t addrlen);
成功返回0,失敗返回-1
2.1.3.1 sockaddr_in結構體
sockaddr_in:保存IPv4地址信息的結構體。
struct sockaddr_in
{sa_family_t sin_family; //地址族uint16_t sin_port; //16位TCP/UDP端口號struct in_addr sin_addr; //32位IP地址char sin_zero[8]; //不使用
}struct in_addr
{in_addr_t s_addr; //32位IPv4地址
}struct sockaddr
{sa_family_t sin_family; //地址族char sa_data[14]; //地址信息
}
數據類型是POSIX(可移植操作系統接口),POSIX是為UNIX系列操作系統設立的標準。
1.sin_family成員
2.sin_port成員
保存16位端口號,是以網絡字節序保存的。
3.sin_addr成員
保存32位IP地址信息,也是以網絡字節序保存的。
4.sin_zero成員
無特殊含義。只是為了使結構體sockaddr_in和sockaddr結構體大小保持一致插入的成員。
為什么我們平常的使用,要先填充 sockaddr_in結構體,再轉換為sockaddr結構體,而不直接填充sockaddr結構體呢?
答:因為sockaddr結構體中sa_data[14]數據的填充很麻煩,其中需包含IP地址和端口號,并且其余部分都要填充為0,才能使用。不如直接使用sockaddr_in結構體,再進行轉換。填充復雜的原因是sockaddr結構體并不僅僅為IPv4而設計。
2.1.4 網絡字節序
不同CPU中,向內存保存數據的方式有兩種,一種是正序,直接保存,一種是倒序保存,這意味著,CPU解析數據的方式也分為兩種:
- 大端序:高位字節存放到低位地址
- 小端序:高位字節存放到高位地址
所以,在數據傳輸時,必須統一方式,這種方式就稱為網絡字節序,即統一為大端序。即先把數據統一轉化為大端序的格式,再進行網絡傳輸,所以在填充sin_addr成員和sin_port成員時需要以網絡字節序保存。
2.1.5 字節序轉換
主機字節序和網絡字節序的相互轉換,被稱為字節序轉換。有以下函數進行轉換:
unsigned short htons(unsigned short); //把short類型數據從主機字節序轉換為網絡字節序
unsigned short ntohs(unsigned short); //把short類型數據從網絡字節序轉換為主機字節序
unsigned long htonl(unsigned long); //把long類型數據從主機字節序轉換為網絡字節序
unsigned long ntohl(unsigned long); //把long類型數據從網絡字節序轉換為主機字節序
htons中的h表示主機(host)字節序。
htons中的n表示網絡(network)字節序。
htons中的s指的是short(short占2個字節,所以常用于端口號的轉換)。
htonl中的l值得是long(Linux中long類型占4個字節,所以常用于IP地址的轉換)。
2.1.6 網絡地址的初始化和分配
2.1.6.1 inet_addr函數
#include <arpa/inet.h>
in_addr_t inet_addr(const char *string);
成功則返回32位大端序整數型值,失敗則返回INADDR_NONE
這個函數幫助我們將字符串形式的IP地址轉換為32位整數型數據,同時也會進行網絡字節序轉換。 同時它也會檢測無效的IP地址。
2.1.6.2 inet_aton函數(Windows不存在此函數)
inet_aton函數和inet_addr函數功能上相同。
#include <arpa/inet.h>
int inet_aton(const char *string,struct in_addr* addr);
成功則返回1,失敗則返回0
string:含有需轉換的IP地址信息的字符串地址值。
addr:將保存轉換結果的in_addr結構體變量的地址值。
2.1.6.3 inet_ntoa函數(和inet_aton函數相反)
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr* addr);
成功則返回轉換的字符串地址值,失敗則返回-1
將網絡字節序32位整數型IP地址轉換為字符串形式。
注意:在使用此函數時,返回的結果是一個指針,指向字符串信息的地址,當第二次使用這個函數時,這個地址存有的字符串信息會被覆蓋掉,所以在使用時,需要立即拷貝保存地址存有的字符串信息。
2.1.6.4 INADDR_ANY常數
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
...
addr.sin_addr.s_addr=htonl(INADDR_ANY);
INADDR_ANY常數:采用這種方式,會自動獲取運行服務器端的計算機的IP地址,不必親自輸入,并且,若同一計算機中分配有多個IP地址(路由器這種),則只要端口號一致,就可以從不同IP地址里接收數據。所以服務器端優先考慮這種方式。
2.1.6.5 WSAStringToAddress函數(只有Windows平臺有,不利于兼容性)
各種類型都是針對默認類型的typedef聲明。
#include <winsock2.h>
INT WSAStringToAddress
(LPTSTR AddressString,INT AddressFamily,LPWSAPROTOCOL_INFO lpProtocolInfo,LPSOCKADDR lpAddress,LPINT lpAddressLength
);
成功返回0,失敗返回SOCKET_ERROR
AddressString:含有IP地址和端口號的字符串地址值
AddressFamily:第一個參數中地址所屬的地址族信息
lpProtocolInfo:設置協議提供者(Provider),默認為NULL
lpAddress:保存地址信息的結構體變量地址值
lpAddressLength:第四個參數中傳遞的結構體長度所在的變量地址值。
2.1.6.6 WSAAddressToString函數(只有Windows平臺有,不利于兼容性)
各種類型都是針對默認類型的typedef聲明。
#include <winsock2.h>
INT WSAAddressToString
(LPSOCKADDR lpsaAddress,DWORD dwAddressLength,LPWSAPROTOCOL_INFO lpProtocolInfo,LPTSTR lpszAddressString,LPDWORD lpdwAddressStringLength
);
成功返回0,失敗返回SOCKET_ERROR
lpsaAddress:需要轉換的地址信息結構體變量地址值
dwAddressLength:第一個參數中結構體的長度
lpProtocolInfo:設置協議提供者(Provider),默認為NULL
lpszAddressString:保存轉換結果的字符串地址值
lpdwAddressStringLength:第四個參數中存有地址信息的字符串長度
2.1.6.7 服務器端初始化IP地址時非常明確,為什么還要進行IP的初始化呢?
因為:同一個計算機可能分配有多個IP地址,實際IP地址和計算機安裝的NIC數量相等。所以服務器需要決定應接收哪個IP地址傳來的數據,所以要服務器端要初始化IP地址。
3.其余流程函數
3.1 進入連接等待狀態(listen函數)
當調用了listen函數,服務器端會阻塞,等待連接請求狀態。意味著,只有在此之后客戶端才能調用connect函數。
#include<sts/socket.h>
int listen(
int sockfd, //希望進入等待連接請求狀態的套接字文件描述符,傳遞的描述符套接字參數為服務器端套接字
int backlog //連接請求等待隊列的長度。
);
成功返回0,失敗返回-1
如圖:
????????客戶端連接請求本身也是從網絡中接收到的一種數據,而接收數據就需要套接字,所以第一個參數服務器端套接字,就是充當門衛,可回復客戶端請求,傳輸"請求已收到"的信號數據。第二個參數就是可以規定,連接請求等候的隊列的大小,一般與服務器端特性有關,像頻繁請求的web端則至少要15。
3.2 受理客戶端連接請求(accpet函數)
#include<sys/socket.h>
int accpet(
int sock, //服務器套接字的文件描述符
struct sockaddr* addr, //保存發起連接請求的客戶端地址信息的變量地址值
socklen_t* addrlen //第二個參數的結構體長度。
);
成功則返回套接字文件描述符,失敗則返回-1
accpet函數,受理連接請求等待隊列中,待處理的客戶端連接請求。函數調用成功,accept函數內部會產生用于數據I/O的套接字,并返回其文件描述符。這個套接字是自動創建的,并自動與發起連接請求的客戶端建立連接。
3.3 客戶端請求連接(connect函數)
#include<sys/socket.h>
int connect(
int sock, //客戶端套接字文件描述符
struct sockaddr* servaddr, //保存目標服務器端地址信息的變量地址值
socklen_t addrlen //第二個參數的變量長度
);
成功返回0,失敗返回-1
?connect函數只有以下情況之一才會返回:
- 服務器端接收連接請求,所謂的“連接請求”,并不意味著服務器端調用accpet函數,而是服務器端把連接請求信息記錄到等待隊列中。所以connect函數返回后并不立即進行數據交換。
- 發生斷網等異常情況而中斷連接請求
4. 基于TCP的服務器端/客戶端函數調用關系
????????圖中的總體流程整理如下:服務器端創建套接字后連續調用bind、listen函數進入等待狀態,客戶端通過調用connect函數發起連接請求。需要注意的是,客戶端只能等到服務器端調用listen函數后才能調connect函數。同時要清楚,客戶端調用connect函數前,服務器端有可能率先調用accept函數。當然,此時服務器端在調用accept函數時進入阻塞( blocking)狀態,直到客戶端調connect函數為止。