Linux環境:C編程之網絡通信
網絡通信概述
計算機網絡通信內容很多,這只是一個針對socket編程的基礎知識概述總結
通信參考模型
互聯網通信遵循一定的通信協議和通信機制,稱爲通信模型。
國際標準化的開放網絡通信模型是OSI參考模型,但是OSI模型概念提出較晚,所以一直沒有實現,只是停留在概念性的參考框架上。目前通用的模型爲TCP/IP通信參考模型,以下主要介紹TCP/IP通信參考模型。
自頂向下的TCP/IP參考模型
應用層
- 客戶在應用層使用各種應用和協議進行通信,應用層實現了應用到應用之間的通信,即進程與進程之間的通信,通過端口識別不同的進程。
- 常用的應用層協議包括http 超文本傳輸協議、ftp 文件傳輸協議 、telnet 遠程登錄 、ssh 安全外殼協議 、stmp 簡單郵件發送協議、pop3 郵件收發協議。應用層程序員可以通過編程實現自己的通信協議。
- 應用層的通信協議規定了用戶傳輸的數據信息的格式,並附加了相關的格式控制信息等,稱爲報文。
- 應用層的報文傳輸由傳輸層實現。
傳輸層
- 傳輸層負責實現端到端通信,即主機到主機之間的通信,源主機ip到目標主機ip
- 傳輸層把應用層的報文按照大小劃分爲報文段並附加傳輸層協議的格式控制信息
- 報文段由傳輸層交付網絡層傳輸。
- 傳輸層從協議主要是TCP協議和UDP協議
- TCP協議通過三次握手(-- 在嗎?-- 我在,你在嗎?-- 我也在,我們開始吧),四次揮手(–我要走了 – 好的我知道了你走吧 – 我也要走了 – 好的再見)機制實現端到端的可靠連接,通過給報文加序號實現亂序重排,並通過滑動窗口協議實現出錯重傳
- UDP協議屬於無連接協議,不可靠,但是實時性高,只負責發送數據,不負責順序發送和出錯重傳。
網絡層
- 網絡層爲傳輸層提供無連接的,盡最大努力交付的數據報服務,主要由IP協議,ICMP,IGMP協議和一系列路由方法協議組成。數據的可靠性需要傳輸層自己保證。
- 網絡層協議負責將傳輸層的報文段封裝成ip分組(數據包),並實現路由尋徑,找到一條從源主機到目的主機的通路。
- 具體的數據流傳輸控制由鏈路層進行。
鏈路層
- 鏈路層協議實現具體的二進制數據流傳輸,對上層掩蓋了物理傳輸的具體細節,是TCP/IP模型的最底層
- 鏈路層協議包括ARP,RARP等協議,實現了ip地址和物理mac地址的對應
- 鏈路層根據物理網絡狀況將ip分組劃分封裝成數據幀進行傳輸,負責數據差錯的檢查和校驗
socket編程預備知識
socket概述
- socket是操作系統內核中的一種數據結構,用來在同一主機或不同主機的進程通信中標識不同進程。
- Linux系統中的socket是一種特殊的I/O接口,同樣由文件描述符進行管理,屬於特殊類型的文件。 ,
- 每一個 socket 都用一個半相關描述{協議、本地地址、本地端口}來表示;一個完整的套接字則用一個相關描述{協議、本地地址、本地端口、遠程地址、遠程端口}來表示。
- 創建一個socket時只需要指明其通信域和通信協議類型即可
- 通信域規定了socket的地址格式和通信範圍
socket類型
- 流式 socket(
SOCK_STREAM
):用於 TCP 通信 - 數據報 socket(
SOCK_DGRAM
) :用於 UDP 通信 - 原始 socket(
SOCK_RAW
) :用於新的網絡協議實現的測試等,可以直接訪問網絡層協議
socket地址類型
socke創建後需要綁定通信地址才能使用,socket的地址格式有兩種如下:兩種地址可以相互轉化
1、sockaddr通用地址
struct sockaddr
{
unsigned short sa_family; /*地址族*/
char sa_data[14]; /*14 字節的協議地址,包含該 socket 的 IP 地址和端口號。*/
};
sockaddr
缺陷:sa_data
把目標地址和端口信息混在一起,不利於網絡通信
2、sockaddr_in網絡地址
struct sockaddr_in
{
short int sin_family; /*地址族*/
unsigned short int sin_port; /*端口號*/
struct in_addr sin_addr; /*IP 地址*/
unsigned char sin_zero[8]; /*填充 0 以保持與 struct sockaddr 同樣大小*/
};
//保存32位二進制ip地址的結構體
struct in_addr
{
unsigned long int s_addr; /* 32 位 IPv4 地址,網絡字節序 */
};
數據存儲字節序——大端小端
- 計算機中的數據按字節存儲,但是有很多數據常常佔據多個字節,這種情況下就需要考慮字節的存儲順序,就像人們的書寫順序一樣。
- 如果先存高位,再存低位,即高位字節對應的內存地址編號小,從地址0讀數據先讀到的是高位時,稱爲大端模式。大端模式同樣也符合人們的書寫閱讀習慣,從左到右先寫高位再寫低位,因此互聯網通信中採用的都是大端模式
- 如果先存低位,再存高位,即低位字節對應的內存地址編號小,就稱爲小端模式。主機中的字節序和處理器相關,市面上的主流處理器如intel都是小端模式。可以通過以下程序判斷主機是大端還是小端模式。
#include <stdio.h> int main() { int x=0x12345678; //佔四個字節,如果是大端的話從低地址到高地址依次是0x12,0x34,0x56,0x78 char * p=(char *)&x; printf("%0x %0x %0x %0x\n",p[0],p[1],p[2],p[3]); //從低位到高位按字節輸出,如果是小端會得到:78,56,34,12 }
- 由於主機字節序和網絡字節序常常有衝突,因此需要進行字節序的轉換,才能正常進行網絡通信,這裏用到四個函數:
uint16_t htons(uint16_t host16bit)
:host to net short 的縮寫,將16位的主機字節序轉換爲16位的網絡字節序並返回,用於16位端口號從主機到網絡的轉換uint16_t ntohs(uint16_t net16bit)
:net to host short 的縮寫,將16位的網絡字節序轉換爲16位的主機字節序並返回,用於16位端口號從網絡到主機的轉換uint32_t htonl(uint32_t host32bit)
:host to net long 的縮寫,將32位的主機字節序轉換爲32位的網絡字節序並返回,用於32位ip地址的轉換uint32_t ntohl(uint32_t net32bit)
:net to host long 的縮寫,將32位的網絡字節序轉換爲32位的主機字節序並返回,用於32位ip地址的轉換
ip地址格式轉換
通常用戶在表達ip地址時採用的是點分十進制或者是冒號分開的十進制 Ipv6 地址。而在socket 編程中使用的則是 32 位的網絡字節序的二進制值,這就需要將這兩個數值進行轉換,轉換用到如下函數:
只適用於ipv4的函數:
int inet_aton(const char *straddr, struct in_addr *addrptr);
將點分十進制數的 IP 地址(straddr
)轉換成爲網絡字節序的 32 位二進制數值(addrptr
),成功,則返回 1,不成功返回 0char *inet_ntoa(struct in_addr inaddr);
將網絡字節序的 32 位二進制數值inaddr
轉換爲點分十進制的 IP 地址返回值unsigned long int inet_addr(const char *straddr);
將點分十進制數的 IP 地址(straddr
)轉換成爲網絡字節序的 32 位二進制數值返回值
兼容ipv6的函數:int inet_pton(int family, const char *src, void *dst);
將ip地址src轉爲網絡字節序的二進制數值,family 參數指定爲 AF_INET,表示是 IPv4 協議,如果是 AF_INET6,表示 IPv6 協議const char *inet_ntop(int family, const void *src, char *dst, socklen_t len);
將二進制數值src轉爲ip地址dst,len表示轉換後的字符串長度
ip地址和域名(主機名)之間的轉換
ip地址不方便記憶,因此實際用戶訪問網絡時通過域名或主機名(局域網)訪問,所以還需要域名(主機名)和ip地址的轉換,準確的說是通過ip或者主機名獲取到主機信息。
主機信息用結構體存放定義如下:
struct hostent
{
char *h_name; /*正式主機名*/
char **h_aliases; /*主機別名*/
int h_addrtype; /*主機 IP 地址類型 IPv4 爲 AF_INET*/
int h_length; /*主機 IP 地址字節長度,對於 IPv4 是 4 字節,即 32 位*/
char **h_addr_list; /*二進制表示的主機的 IP 地址列表,一個域名或主機可能對應多個ip*/
}
struct hostent* gethostbyname(const char* hostname);
參數爲主機名,返回主機信息結構體指針,指針爲NULL表示查找失敗struct hostent* gethostbyaddr(const char* addr,size_t len,int family);
參數爲ip地址的二進制表示,len爲地址長度,family爲地址類型
socket編程
TCP通信協議編程
TCP 協議 socket 通信過程:
服務端:socket—bind—listen—while(1){—accept—recv—send—close—}---close
客戶端:socket----------------------------------connect—send—recv-----------------close
服務器端創建socket,用bind綁定服務器的ip和對應進程的端口號,然後通過listen函數註冊爲被動socket,等待客戶端進程的主動連接,通過rev接收消息,send發送消息,close關閉連接,然後繼續監聽等待連接
客戶端創建socket,調用connect主動連接到一個被動socket,發送數據,接收回應,然後關閉連接
socket函數
- 作用:創建一個socket,返回其文件描述符
- 原型:
int socket(int domain,int type,int protocol);
- 參數:
domain
:通信域,AF_INET
:Ipv4 網絡協議 ,AF_INET6
:IPv6 網絡協議,AF_UNIX
:內核通信type
:SOCK_STREAM
:TCP協議,流式socket;SOCK_DGRAM
:UDP協議,數據包socketprotocol
:指定 socket 所使用的傳輸協議編號。通常爲 0
- 返回值:成功則返回socket描述符,失敗返回-1
bind函數
- 作用:將一個socket與一個地址結構綁定,使其與指定的端口號和 IP 地址相關聯
- 原型:
int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
- 參數:
sockfd
:socket文件描述符sockaddr
:地址結構指針,結構體類型和地址的類型相關addrlen
:sizeof(struct sockaddr),地址結構體的長度
- 返回值:成功則返回 0,失敗返回-1
listen 函數
- 作用:listen函數將一個socket註冊爲被動socket,用來監聽等待其他socket的主動connect。一個成功connect的socket無法再註冊爲被動socket
- 原型:
int listen(int sockfd,int backlog);
- 參數:
sockfd
爲套接字描述符,backlog
可以理解爲該socket同時能處理的最大連接要求,通常爲 5 或者 10,最大值可設至 128.。 - 返回值:成功則返回 0,失敗返回-1
accept函數
- 作用:在監聽socket上接受一個調用了connect的主動socket的連接,如果不存在接入連接則阻塞等待連接。調用accept成功後會創建一個新的socket與發起連接的socekt建立連接並進行後續通信。
- 原型:
int accept(int sfd,struct sockaddr * addr,int * addrlen);
- 參數:
sfd
:傳入參數,socket描述符,服務端的socket描述符addr
:傳出參數,創建的新socket的地址結構體指針,如果不需要獲取該socket地址時可以傳入NULL。addrlen
:調用時填addr指針指向的緩衝區大小,傳出時變爲addr指向的結構體實際佔用的大小
- 返回值:成功則返回 創建的新socket的文件描述符,失敗返回-1
connect函數
- 作用:用來請求連接一個指定ip和端口號的處於監聽狀態的被動socket
- 原型:
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
- 參數:
sockfd
:申請連接的主動socket的文件描述符serv_addr
:爲結構體指針變量,存儲着服務端被動socket的 IP 與端口號信息。addrlen
:表示serv_addr指向的結構體變量的長度
- 返回值:成功則返回 0,失敗返回-1
I/O函數
socket是文件類型,因此可以使用read和write函數進行讀寫。
但是,由於socket用於網絡通信,因此在使用read和write函數時可能會出現一些問題。
比如write函數只負責將數據寫入本地socket的緩衝區中,並不保證全部數據都能寫入,可能會由於網絡連接中斷等原因導致數據沒有完全寫入,需要自行控制重新寫入,
read函數雖然可以讀取指定長度的字符,但是可能對方的信息還沒有完全發送完畢,這時候可能只讀了一部分數據。
爲了解決這些問題,在socket通信中使用recv和send函數來進行讀寫,在這兩個函數中多了控制信息參數。
recv函數
-
作用:用新的套接字來接收遠端主機傳來的數據,並把數據存到由參數 buf 指向的內存空間
-
原型:int recv(int sockfd,void *buf,int len,unsigned int flags);
-
參數:
sockfd
:接收方socket的文件描述符buf
:指向接收數據的緩衝區len
:表示緩衝區的長度,即指定的接收長度flags
:0,此時和read函數沒有區別;MSG_PEEK:表示只是從系統緩衝區中讀取內容,而不清除系統緩衝區的內容.這樣下次讀的時候,仍然是一樣的內容.一般在有多個進程讀寫數據時可以使用這個標誌.;MSG_WAITALL表示等到所有的信息到達時才返回.使用這個標誌的時候recv回一直阻塞,直到指定的條件滿足,或者是發生了錯誤.
-
返回值:成功則返回實際接收到的字符數,可能會少於指定的接收長度(讀到了文件結尾)。失敗返回-1。
send函數
- 作用:使用socket發送數據
- 原型:
int send(int sfd,const void * msg,int len,unsigned int flags);
- 參數:前三個參數和write函數一致,sfd是發送方socket的文件描述符,重點在
flags
的取值:- flags=0:和write函數功能一致。默認爲阻塞模式,即緩衝區有數據未發送時會阻塞等待
- flags=MSG_DONTWAIT:send以非阻塞方式運行,發送緩衝區被佔用時立即返回
- flags=MSG_NOSIGNAL:send發送時對方關閉連接,如果指定該標誌量則發送進程不會收到SIGPIPE信號。
- 返回值:成功則返回實際傳送出去的字符數,可能會少於指定的發送長度。失敗返回-1。
- 如果send在等待協議傳送數據時網絡斷開的話,調用send的進程會接收到SIGPIPE信號,進程對該信號的默認處理是進程終止
close函數
socket同樣是文件,故只需調用close關閉文件的方式關閉即可。
需要注意的是,調用close關閉套接字時,雙向通信的兩端都會關閉,不能讀也不能寫。
shutdown函數
- 作用:可以只關閉一端的連接,數據還可以在一個方向上傳輸。同時,關閉的時候,所有的文件描述符都會失效。
- 原型:
int shutdown(int sockfd,int how)
: - 參數:
socket:
文件描述符how
:SHUT_RD
:關閉讀端 ,之後再進行讀操作返回文件結尾。對等端進行寫操作則會收到SIGPIPE信號,繼續寫會報EPIPE錯誤。SHUT_WR
:關閉寫端。對等端讀操作的時候會讀到文件結尾,本地寫操作將產生SIGPIPE信號和EPIPE錯誤。SHUT_RDWR
:關閉讀寫端
- 返回值:成功返回0,失敗返回-1。
UDP通信協議 編程
UDP協議協議 socket 通信過程:
服務端:socket—bind—recvfrom—sendto—close
客戶端:socket----------sendto—recvfrom—close
UDP通信類似收發快遞:
服務端創建數據報socket,相當於創建快遞櫃,調用recvfrom函數來接收數據報,沒有則阻塞。sendto相當於發快遞,需要指明接收方的地址才能發送出去。
recvfrom函數
- 作用:接收一個數據報socket傳來的數據報,並可以獲得發送方的地址
- 原型:
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);
- 參數:
- 前四個參數和
recv
函數一致, from
參數是傳出參數,傳出發送方的地址結構體指針fromlen
參數也是傳出參數,標識了from
的長度
- 前四個參數和
- 返回值:成功則返回實際接收到的字符數,可能會少於指定的接收長度(讀到了文件結尾)。失敗返回-1。
sendto函數
- 作用:向指定地址的socket發送數據報
- 原型:
int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
- 參數:
- 前四個參數和
send
函數一致, to
參數指定接收方的地址結構體tolen
參數標識了to
指向的結構體的長度
- 前四個參數和
- 返回值:成功則返回實際傳送出去的字符數,可能會少於指定的發送長度。失敗返回-1。