在TCP/IP協議中,“IP地址+TCP或UDP端口號”唯一標識網絡通訊中的一個進程,“IP地址+端口號”就稱爲socket。
在TCP協議中,建立連接的兩個進程各自有一個socket來標識,那麼這兩個socket組成的socket pair就唯一標識一個連接。socket本身有“插座”的意思,因此用來描述網絡連
接的一對一關係。
TCP/IP協議最早在BSD UNIX上實現,爲TCP/IP協議設計的應用層編程接口稱爲socketAPI。
本章的主要內容是socket API,主要介紹TCP協議的函數接口,最後介紹UDP協議和UNIX Domain Socket的函數接口。
圖1-1socket-api調用流程
1.1 基礎知識
網絡字節序
我們已經知道,內存中的多字節數據相對於內存地址有大端和小端之分,磁盤文件中的多字節數據相對於文件中的偏移地址也有大端小端之分。網絡數據流同樣有大端小端之分,那麼如何定義網絡數據流的地址呢?發送主機通常將發送緩衝區中的數據按內存地址從低到高的順序發出,接收主機把從網絡上接到的字節依次保存在接收緩衝區中,也是按內存地址從低到高的順序保存,因此,網絡數據流的地址應這樣規定:先發出的數據是低地址,後發出的數據是高地址。
TCP/IP協議規定,網絡數據流應採用大端字節序,即低地址高字節。例如上一節的UDP段格式,地址0-1是16位的源端口號,如果這個端口號是1000(0x3e8),則地址0是0x03,地址1是0xe8,也就是先發0x03,再發0xe8,這16位在發送主機的緩衝區中也應該是低地址存0x03,高地址存0xe8。但是,如果發送主機是小端字節序的,這16位被解釋成0xe803,而不是1000。因此,發送主機把1000填到發送緩衝區之前需要做字節序的轉換。同樣地,接收主機如果是小端字節序的,接到16位的源端口號也要做字節序的轉換。如果主機是大端字節序的,發送和接收都不需要做轉換。同理,32位的IP地址也要考慮網絡字節序和主機字節序的問題。
爲使網絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯後都能正常運行,可以調用以下庫函數做網絡字節序和主機字節序的轉換。
#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位短整數。 //如果主機是小端字節序,這些函數將參數做相應的大小端轉換然後返回,如果主機是大端字節序,這些函數不做轉換,將參數原封不動地返回。 |
IP地址轉換函數
#include <arpa/inet.h> int inet_pton(int af, const char *src, void *dst);//把字符串的ip轉換成32位二進制的整型 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);//把32位二進制的整型轉成字符串的ip //支持IPv4和IPv6 可重入函數 |
其中inet_pton和inet_ntop不僅可以轉換IPv4的in_addr,還可以轉換IPv6的in6_addr,因此函數接口是void *addrptr
sockaddr數據結構
strcut sockaddr 很多網絡編程函數誕生早於IPv4協議,那時候都使用的是sockaddr結構體,爲了向前兼容,現在sockaddr退化成了(void *)的作用,傳遞一個地址給函數,至於這個函數是sockaddr_in還是sockaddr_in6,由地址族確定,然後函數內部再強制類型轉化爲所需的地址類型。內存結構如下圖所示:
struct sockaddr { sa_family_t sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */ };
//ipv4 struct sockaddr_in { __kernel_sa_family_t sin_family; /* Address family */ __be16 sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ /* Pad to size of `struct sockaddr'. */ unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)];//填充字節 }; /* Internet address. */ struct in_addr { __be32 s_addr; };
//ipv6 struct sockaddr_in6 { unsigned short int sin6_family; /* AF_INET6 */ __be16 sin6_port; /* Transport layer port # */ __be32 sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ __u32 sin6_scope_id; /* scope id (new in RFC2553) */ };
struct in6_addr { union { __u8 u6_addr8[16]; __be16 u6_addr16[8]; __be32 u6_addr32[4]; } in6_u;
#define s6_addr in6_u.u6_addr8 #define s6_addr16 in6_u.u6_addr16 #define s6_addr32 in6_u.u6_addr32 };
//UNIX Domain Socket的地址格式 #define UNIX_PATH_MAX 108 struct sockaddr_un { __kernel_sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname 類似於有名管道*/ }; |
Pv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結構體表示,包括16位端口號和32位IP地址,IPv6地址用sockaddr_in6結構體表示,包括16位端口號、128位IP地址和一些控制字段。UNIXDomain Socket的地址格式定義在sys/un.h中,用sockaddr_un結構體表示。各種socket地址結構體的開頭都是相同的,前16位表示整個結構體的長度(並不是所有UNIX的實現都有長度字段,如Linux就沒有),後16位表示地址類型。IPv4、IPv6和Unix Domain Socket的地址類型分別定義爲常數AF_INET、AF_INET6、AF_UNIX。這樣,只要取得某種sockaddr結構體的首地址,不需要知道具體是哪種類型的sockaddr結構體,就可以根據地址類型字段確定結構體中的內容。因此,socket API可以接受各種類型的sockaddr結構體指針做參數,例如bind、accept、connect等函數,這些函數的參數應該設計成void *類型以便接受各種類型的指針,但是sock API的實現早於ANSI C標準化,那時還沒有void *類型,因此這些函數的參數都用struct sockaddr *類型表示,在傳遞參數之前要強制類型轉換一下,例如:
struct sockaddr_in servaddr; /* initialize servaddr */ bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));//傳遞參數時強轉 |
1.2網絡套接字API
1.2.1 socket()(構造出一條通道)
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol);
domain: AF_INET6 //與上面類似,不過是來用IPv6的地址 AF_UNIX //本地協議,使用在Unix和Linux系統上,一般都是當客戶端和服務器在同一臺及其上的時候使用 PF_UNIX/PF_LOCAL/AF_UNIX/AF_LOCAL //UNIX 進程通信協議 PF_INET/AF_INET //Ipv4網絡協議,這是大多數用來產生socket的協議,使用TCP或UDP來傳輸,用IPv4的地址 PF_INET6/AF_INET6 //Ipv6網絡協議 PF_IPX/AF_IPX IPX-Novell //協議 PF_NETLINK/AF_NETLINK //核心用戶接口裝置 PF_X25/AF_X25 //ITU-T X. 25/ISO-8208 協議 PF_AX25/AF_AX25 //業餘無線AX. 25 協議 PF_ATMPVC/AF_ATMPVC //存取原始 ATM PVCs PF_APPLETALK/AF_APPLETALK //appletalk (DDP)協議 PF_PACKET/AF_PACKET //初級封包接口 type: SOCK_STREAM 這個協議是按照順序的、可靠的、數據完整的基於字節流的連接。這是一個使用最多的socket類型,這個socket是使用TCP來進行傳輸。 SOCK_DGRAM 這個協議是無連接的、固定長度的傳輸調用。該協議是不可靠的,使用UDP來進行它的連接。 SOCK_SEQPACKET 這個協議是雙線路的、可靠的連接,發送固定長度的數據包進行傳輸。必須把這個包完整的接受才能進行讀取 SOCK_RAW 這個socket類型提供單一的網絡訪問,這個socket類型使用ICMP公共協議。(ping、traceroute使用該協議) SOCK_RDM 這個類型是很少使用的,在大部分的操作系統上沒有實現,它是提供給數據鏈路層使用,不保證數包的順序 protocol: 0 默認協議 返回值: 成功返回一個新的文件描述符,失敗返回-1,設置errno |
socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣返回一個文件描述符,應用程序可以像讀寫文件一樣用read/write在網絡上收發數據,如果socket()調用出錯則返回-1。對於IPv4,domain參數指定爲AF_INET。對於TCP協議,type參數指定爲SOCK_STREAM,表示面向流的傳輸協議。如果是UDP協議,則type參數指定爲SOCK_DGRAM,表示面向數據報的傳輸協議。protocol參數的介紹從略,指定爲0即可。
socket類型:
當socket創建完之後,程序必須指定地址域和socket類型。兩個進程能夠相互通信,當且僅當它們的socket是相同類型的並且在相同的域。
有兩種廣泛使用的地址域,一種是Unix域(unix domain),兩個進程共享一套文件系統進行通信,另一種是互聯網域(Internet domain),兩個運行在互聯網主機上的進程進行通信。這兩種地址域有自己的地址格式。
在Unix域中,socket的地址是文件系統中的字符串條目。
在互聯網域中,socket的地址包含一個主機的網絡地址(網絡中的每臺計算機都有一個唯一的32位地址,通常稱爲它的IP地址),另外,每一個socket需要一個主機的端口號。端口號是16位的無符號整形數。其中比較小的數是系統保留的,綁定一些標準服務。例如,FTP服務器的端口號是21.所有計算機上的標準服務都有相同的端口號十分重要,這樣,客戶端才能知道它們的地址,以方便連接。通常,大於2000的端口號是可用的。
有兩種廣泛使用的socket類型,一種是流socket(stream socket),另一種是數據報socket(datagram socket)。流socket把通信當做一個連續的字符串流,而數據報socket必須把整條信息一次讀完。每種類型有他們自己的通信協議。
流socket使用TCP協議(transmissioncontrol protocol,傳輸控制協議),TCP協議是穩定的、面向流的協議;數據報socket使用UDP協議(Unix datagram protocol),UDP協議是不穩定的,並且是面向消息的。
1.2.2 bind()
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h>
//綁定後,若有用戶訪問addr地址時,會通過sockfd進行數據傳遞 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: socket文件描述符 addr: 構造出IP地址加端口號 addrlen: sizeof(addr)長度 返回值: 成功返回0,失敗返回-1, 設置errno。轉到 |
服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號後就可以向服務器發起連接,因此服務器需要調用bind綁定一個固定的網絡地址和端口號。
bind()的作用是將參數sockfd和addr綁定在一起,使sockfd這個用於網絡通訊的文件描述符監聽addr所描述的地址和端口號。前面講過,struct sockaddr *是一個通用指針類型,addr參數實際上可以接受多種協議的sockaddr結構體,而它們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度。
//sockaddr_in對象的設置 struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); //置字節字符串前n個字節爲零且包括‘\0’ servaddr.sin_family = AF_INET; //地址家族 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //綁定ip地址,INADDR_ANY表示本機的任意一個IP地址都可以 servaddr.sin_port = htons(8000);//綁定端口 |
首先將整個結構體清零,然後設置地址類型爲AF_INET,網絡地址爲INADDR_ANY,這個宏表示本地的任意IP地址,因爲服務器可能有多個網卡,每個網卡也可能綁定多個IP地址,這樣設置可以在所有的IP地址上監聽,直到與某個客戶端建立了連接時才確定下來到底用哪個IP地址,端口號爲8000。
1.2.3 listen()
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h>
//讓sockfd所指向的socktet具有監聽的能力 int listen(int sockfd, int backlog);
sockfd: socket文件描述符 backlog: 排隊建立3次握手隊列和剛剛建立3次握手隊列的鏈接數和(默認爲128) 返回值: 成功返回0,失敗-1並設置errno。轉到 |
這個函數特別適用於同時有多個連接請求的服務器;
查看系統默認backlog:cat/proc/sys/net/ipv4/tcp_max_syn_backlog
典型的服務器程序可以同時服務於多個客戶端,當有客戶端發起連接時,服務器調用的accept()返回並接受這個連接,如果有大量的客戶端發起連接而服務器來不及處理,尚未accept的客戶端就處於連接等待狀態,listen()聲明sockfd處於監聽狀態,並且最多允許有backlog個客戶端處於連接待狀態,如果接收到更多的連接請求就忽略。listen()成功返回0,失敗返回-1。
若客戶端(ip+端口)向服務器發起鏈接,以下這些過程都是在內核進行的,在經過socket和bind函數後,服務器創建出一個socket(和ip+端口號綁定),在建立連接時候,TCP是通過三次握手建立,內核中會出兩個隊列,一個剛剛3次握手成功,另個是等待3次握手(三次握手整個過程沒有完全完成)。隊列長度有限,若隊列滿了,再來信號,報錯RST。accept阻塞在socket,監聽等待。若accept接受到連接,返回一個socket的文件描述符,專門用於和發起鏈接的客戶端通信。connect負責發起連接,建立一個socket(會臨時分配一個端口號),向服務端發數據。
1.2.4 accept()
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf: socket文件描述符 addr: 傳出參數,返回鏈接客戶端地址信息,含IP地址和端口號 addrlen: 傳入傳出參數(值-結果),傳入sizeof(addr)大小,函數返回時返回真正接收到地址結構體的大小(IPv4或IPv6) 返回值: 成功返回一個新的socket文件描述符,用於和客戶端通信,失敗返回-1,設置errno |
三方握手完成後,服務器調用accept()接受連接,如果服務器調用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來。addr是一個傳出參數,accept()返回時傳出客戶端的地址和端口號。addrlen參數是一個傳入傳出參數(value-resultargument),傳入的是調用者提供的緩衝區addr的長度以避免緩衝區溢出問題,傳出的是客戶端地址結構體的實際長度(有可能沒有佔滿調用者提供的緩衝區)。如果給addr參數傳NULL,表示不關心客戶端的地址。
服務器程序結構是這樣的:
while (1) { cliaddr_len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); n = read(connfd, buf, MAXLINE); ...... close(connfd); } |
1.2.5 connect()
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf: socket文件描述符 addr: 傳入參數,指定服務器端地址信息,含IP地址和端口號 addrlen: 傳入參數,傳入sizeof(addr)大小 返回值: 成功返回0,失敗返回-1,設置errno |
客戶端需要調用connect()連接服務器,connect和bind的參數形式一致,區別在於bind的參數是自己的地址,而connect的參數是對方的地址。connect()成功返回0,出錯返回-1。
1.2.6 tcp數據讀寫
recv(int fd,void*buf,size_t len,int flags) send(int fd,const void *buf,size_t len,int flags) flag 有一些特別的參數 持續監聽對方的迴應 不經過路由表 對socket非阻塞 發送緊急數據 不接受sig信號等等 |
1.2.7 udp數據讀寫
recvfrom(int fd,void*buf,size_t len,int flags,struct sockaddr * my_addr, int addrlen) sendto(int fd,const void*buf,size_t len,int flags,struct sockaddr * my_addr, int addrlen) |
1.2.8 通用數據讀寫
recvmsg(int fd,struct msghdr*msg,int flags); sendmsg(int fd,struct msghdr*msg,int flags); |
struct msghdr { void* msg_name; socklen_t msg_namelen; struct iovec*msg_iov; int msg_iovlen; void* msg_control; socklen_t msg_controllen; int msg_flags; };
struct iovec { void *iov_base; size_t iov_len; }; |
readv和writev
readv和writev函數是Linux中的兩個系統調用,類似於read和write函數,不同的是,readv和writev在一次執行過程中可以原子地作用於多個緩衝區,這些緩衝區常常是非連續的。readv和writev的原型如下:
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt); ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec { void *iov_base; /* Starting address */ size_t iov_len; /* Number of bytes to transfer */ };
參數: int fd:是個文件描述符, const struct iovec *iov:是指向iovec數據結構的一個指針,其中iov_base爲緩衝區首地址,iov_len爲緩衝區長度; int iovcnt:指定了iovec的個數。 返回值: 函數調用成功時返回讀、寫的總字節數,失敗時返回-1並設置相應的errno。 |
在一次函數調用中,writev以順序iov[0]、iov[1]至iov[iovcnt-1]從各緩衝區中聚集輸出數據到fd,readv則將從fd讀入的數據按同樣的順序散佈到各緩衝區中,readv總是先填滿一個緩衝區,然後再填下一個,因此,writev稱爲gather output,readv稱爲scatter input。
先來看一個writev的例子,指定了兩個緩衝區,str0和str1,內容輸出到標準輸出,並打印實際輸出的字節數。
// writevex.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/uio.h>
int main() { char *str0 = "hello "; char *str1 = "world\n"; struct iovec iov[2]; ssize_t nwritten;
iov[0].iov_base = str0; iov[0].iov_len = strlen(str0); iov[1].iov_base = str1; iov[1].iov_len = strlen(str1);
nwritten = writev(STDOUT_FILENO, iov, 2); printf("%ld bytes written.\n", nwritten);
exit(EXIT_SUCCESS); } |
1.2.9帶外標記判斷
int sockatmark(int sockfd); |
1.2.10地址信息函數
int getsockname(int socketfd,struct sockaddr * my_addr, int addrlen);//本端的地址信息 int getpeername(int socketfd,struct sockaddr * my_addr, int addrlen);//遠端的地址信息 |
1.2.11 socket選項
//獲取socket選項 getsockopt(int socketfd,int opt_name,void* option_value,socket_t restrict option_len) //設置socket選項 getsockopt(int socketfd,int opt_name,void* option_value,socket_t restrict option_len) |
選項名稱 說明 數據類型 ======================================================================== SOL_SOCKET ------------------------------------------------------------------------ SO_BROADCAST 允許發送廣播數據 int SO_DEBUG 允許調試 int SO_DONTROUTE 不查找路由 int SO_ERROR 獲得套接字錯誤 int SO_KEEPALIVE 保持連接 int SO_LINGER 延遲關閉連接 struct linger SO_OOBINLINE 帶外數據放入正常數據流 int SO_RCVBUF 接收緩衝區大小 int SO_SNDBUF 發送緩衝區大小 int SO_RCVLOWAT 接收緩衝區下限 int SO_SNDLOWAT 發送緩衝區下限 int SO_RCVTIMEO 接收超時 struct timeval SO_SNDTIMEO 發送超時 struct timeval SO_REUSERADDR 允許重用本地地址和端口 int SO_TYPE 獲得套接字類型 int SO_BSDCOMPAT 與BSD系統兼容 int ======================================================================== IPPROTO_IP ------------------------------------------------------------------------ IP_HDRINCL 在數據包中包含IP首部 int IP_OPTINOS IP首部選項 int IP_TOS 服務類型 IP_TTL 生存時間 int ======================================================================== IPPRO_TCP ------------------------------------------------------------------------ TCP_MAXSEG TCP最大數據段的大小 int TCP_NODELAY 不使用Nagle算法 int ======================================================================== |
1.2.12獲取主機信息
//根據名字獲取主機信息 struct hostent* gethostbyname(const char *name) //根據ip獲取主機信息 struct hostent* gethostbyaddr(const void *addr,size_t len,int type) |
struct hostent { char *h_name;//主機名 char** h_aliases;//主機別名,可能有多個 int h_addrtype;//地址類型 int h_length;// 地址長度 char ** h_addr_list//按照網絡字節序列列出idp地址列表 } |
1.2.13獲取服務信息
//根據 名字,端口號獲取服務信息 struct servent*getservbyname(const char *name,const char *proto); //根據服務類型,端口號獲取服務信息 struct servent*getservbyport(int port,const char *proto); |
struct servent{ char *h_name;//主機名 char** h_aliases;//主機別名,可能有多個 int s_port;//端口號 char *s_proto;//服務類型,tcp或者upd等 } |
1.2.14通過主機名獲取ip地址
int getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result ); |
1.2.15通過socket地址獲取主機名
int getnameinfo (const struct sockaddr *sockaddr, socklen_t addrlen, char *host, socklen_t hostlen, char *serv, socklen_t servlen, int flags); |
1.2.16 套接字關閉shutdown / close
(1)shutdown
#include <sys/socket.h> // 可以是全雙工的sockfd部分或全部停工 int shutdown(int sockfd, int how); | ||||||||
參數: int sockfd: 要操做的套接字,要求有效 int how: SHUT_RD(0): 關閉sockfd上的讀功能,將不允許sockfd進程讀操作 SHUT_WR(1): 關閉sockfd的寫功能,將不允許sockfd進行寫操作 SHUT_RDWR(2): sockfd不再可讀可寫
返回值: On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
|
(2)close
#include <unistd.h> //關閉一個打開的文件描述符 int close(int fd); | ||||||||
返回: close() returns zero on success. On error, -1 is returned, and errno is set appropriately.
|
(3)close與shutdown區別
l close終止了數據傳送的兩個方向
l shutdown可以有選擇的指針某個方向的數據傳送或者終止數據傳送的兩個方向
l shutdown how=SHUT_WR就可以保證對等方接受到一個EOF字符,而不管其他進程是否已經打開了套接字。而close不能保證,直到套接字引用計數減爲0時才發送。也就是說直到所有的進程都關閉了套接字,纔會發送EOF。
例如:
int conn = accept(sock, NULL, NULL); pid_t pid = fork(); if(pid == -1) exit(EXIT_FAILURE); if(pid == 0){ //子進程 close(sock); //通信 close(conn); //這時纔會向對方發送FIN段(因爲這個時候conn引用記數減爲0) } else if(pid > 3){ //父進程 close(conn); //不會向客戶端發送FIN段,僅僅是將套接字的引用記數減1 } |