1.socket基本操作
1.1 socket()函數
int socket(int domain, int type, int protocol);
socket()用于創建一個socket描述符(socket descriptor),它唯一標識一個socket。這個socket描述字跟文件描述字一樣,后續的操作都有用到它,把它作為參數,通過它來進行一些讀寫操作。
- 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(UDP)、SOCK_RAW、SOCK_PACKET(TCP)、SOCK_SEQPACKET等等
- protocol:故名思意,就是指定協議。常用的協議有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議
socket創建一個socket時,返回的socket描述字它存在于協議族(address family,AF_XXX)空間中,但沒有一個具體的地址。如果想要給它賦值一個地址,就必須調用bind()函數,否則就當調用connect()、listen()時系統會自動隨機分配一個端口。
1.2 bind()函數
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()函數把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。
- sockfd:即socket描述字,它是通過socket()函數創建了,唯一標識一個socket。bind()函數就是將給這個描述字綁定一個名字
- addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址
- addrlen:對應的是地址的長度
通常服務器在啟動的時候都會綁定一個眾所周知的地址(如ip地址+端口號),用于提供服務,客戶就可以通過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是為什么通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個
1.2.1 網絡字節序與主機字節序
網絡字節序與主機字節序
1)主機字節序就是我們平常說的大端和小端模式:不同的CPU有不同的字節序類型,這些字節序是指整數在內存中保存的順序,這個叫做主機序。引用標準的Big-Endian和Little-Endian的定義如下:
- Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。
- Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。
2) 網絡字節序:4個字節的32 bit值以下面的次序傳輸:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。這種傳輸次序稱作大端字節序。由于TCP/IP首部中所有的二進制整數在網絡中傳輸時都要求以這種次序,因此它又稱作網絡字節序。
在將一個地址綁定到socket的時候,請先將主機字節序轉換成為網絡字節序,而不要假定主機字節序跟網絡字節序一樣使用的是Big-Endian。
1.3 listen()、connect()函數
如果作為一個服務器,在調用socket()、bind()之后就會調用listen()來監聽這個socket,如果客戶端這時調用connect()發出連接請求,服務器端就會接收到這個請求。
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函數:socket()函數創建的socket默認是一個主動類型的,listen函數將socket變為被動類型的,等待客戶的連接請求。
- sockfd即為要監聽的socket描述字
- backlog為相應socket可以排隊的最大連接個數。
connect函數
- sockfd即為客戶端的socket描述字
- addr為服務器的socket地址
- addrlen為socket地址的長度。客戶端通過調用connect函數來建立與TCP服務器的連接
1.4 accept()函數
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd 為服務器的socket描述字
- addr為指向struct sockaddr *的指針,用于返回客戶端的協議地址
- addrlen為協議地址的長度。
如果accpet成功,那么其返回值是由內核自動生成的一個全新的描述字,代表與返回客戶的TCP連接。
注意:accept的第一個參數為服務器的socket描述字,是服務器開始調用socket()函數生成的,稱為 監聽socket描述字;而accept函數返回的是 已連接的socket描述字。一個服務器通常通常僅僅只創建一個監聽socket描述字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建了一個已連接socket描述字,當服務器完成了對某個客戶的服務,相應的已連接socket描述字就被關閉。
1.5 read(),write()等函數
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
read函數是負責從fd中讀取內容.當讀成功時,read返回實際所讀的字節數,如果返回的值是0表示已經讀到文件的結束了,小于0表示出現了錯誤。
write函數將buf中的nbytes字節內容寫入文件描述符fd.成功時返回寫的字節數。失敗時返回-1,并設置errno變量。
1.6 close()函數
完成了讀寫操作就要關閉相應的socket描述字
int close(int fd);
2.TCP協議通信流程
2.1 TCP三次握手
服務器調用 socket()、bind()、listen() 函數完成初始化后,調用 accept() 阻塞等待,處于監聽端口的狀態,客戶端調用 socket() 初始化后,調用 connect() 發出 SYN 段并阻塞等待服務器應答,服務器應答一個SYN-ACK 段,客戶端收到后從 connect() 返回,同時應答一個 ACK 段,服務器收到后從 accept() 返回。
2.2 TCP數據傳輸
TCP 連接建立后數據傳輸的過程:
建立連接后,TCP 協議提供全雙工的通信服務,但是一般的客戶端/服務器程序的流程是由客戶端主動發起請求,服務器被動處理請求,一問一答的方式。因此,服務器從 accept() 返回后立刻調用 read(),讀 socket 就像讀管道一樣,如果沒有數據到達就阻塞等待,這時客戶端調用 write() 發送請求給服務器,服務器收到后從 read() 返回,對客戶端的請求進行處理,在此期間客戶端調用 read() 阻塞等待服務器的應答,服務器調用 write() 將處理結果發回給客戶端,再次調用 read() 阻塞等待下一條請求,客戶端收到后從 read() 返回,發送下一條請求,如此循環下去。
2.3 四次揮手
如果客戶端沒有更多的請求了,就調用 close() 關閉連接,就像寫端關閉的管道一樣,服務器的 read() 返回 0,這樣服務器就知道客戶端關閉了連接,也調用 close() 關閉連接。注意,任何一方調用 close() 后,連接的兩個傳輸方向都關閉,不能再發送數據了。如果一方調用 shutdown() 則連接處于半關閉狀態,仍可接收對方發來的數據