目錄
1.前言
2.read()-write()函數
3.readv()-writev()函數
4.send()-recv()函數
4.1.send()函數說明
4.2.recv()函數說明
5.sendmsg()-recvmsg()函數
6.sendto()-recvfrom()函數
6.1.sendto()函數說明
6.2.recvfrom()函數說明
7.close()函數
8.shutdown()函數
9.close與shutdown差別
1.前言
本篇文章的所有例子,基于RHEL6.5平台(linux kernal: 2.6.32-431.el6.i686)。
在前一篇文章中《程序間通信(10) - 網絡套接字1(socket)》,已經介紹了socket(),bind(),listen(),connect(),accept()這些函數。
至此,伺服器與客戶機已經建立好了連接配接。可以調用網絡I/O進行讀寫操作了,即實作網絡中不同程序之間的通信。網絡I/O操作有下面的幾組函數:
· read() / write()
· readv() / writev()
· send() / recv()
· sendmsg() / recvmsg()
· sendto() / recvfrom()
下面的幾個小節會對這些函數進行詳細介紹。
2.read()-write()函數
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
read函數負責從fd中讀取内容。當讀成功時,read傳回實際所讀的位元組數,如果傳回的值是0表示已經讀到檔案的結束了,<0表示出現了錯誤。如果錯誤為EINTR說明讀是由中斷引起的,如果是ECONNREST表示網絡連接配接出了問題。
write函數将buf中的count位元組内容寫入檔案描述符fd.成功時傳回寫的位元組數。失敗時傳回-1,并設定errno變量。 在網絡程式中,當我們向套接字檔案描述符寫時有2種可能:
1)write的傳回值大于0,表示寫了部分或者是全部的資料。
2)傳回的值小于0,此時出現了錯誤。我們要根據錯誤類型來處理。如果錯誤為EINTR表示在寫的時候出現了中斷錯誤。如果為EPIPE表示網絡連接配接出現了問題(對方已經關閉了連接配接)。
3.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 */
};
參數fd是檔案描述字。
參數iov是一個結構數組,它的每個元素指明存儲器中的一個緩沖區。結構類型iovec有下述成員,分别給出緩沖區的起始位址和位元組數。
參數iovcnt指出數組iov的元素個數,元素個數至多不超過IOV_MAX。Linux中定義IOV_MAX的值為1024。
read()和write()系統調用每次在檔案和程序的位址空間之間傳送一塊連續的資料。但是,應用有時也需要将分散在記憶體多處地方的資料連續寫到檔案中,或者反之。在這種情況下,如果要從檔案中讀一片連續的資料至程序的不同區域,使用read()則要麼一次将它們讀至一個較大的緩沖區中,然後将它們分成若幹部分複制到不同的區域,要麼調用read()若幹次分批将它們讀至不同區域。同樣,如果想将程式中不同區域的資料塊連續地寫至檔案,也必須進行類似的處理。
UNIX提供了另外兩個函數—readv()和writev(),它們隻需一次系統調用就可以實作在檔案和程序的多個緩沖區之間傳送資料,免除了多次系統調用或複制資料的開銷。
readv()稱為散布讀(scatter read),即将檔案中若幹連續的資料塊讀入記憶體分散的緩沖區中。
writev()稱為聚集寫(gather write),即收集記憶體中分散的若幹緩沖區中的資料寫至檔案的連續區域中。
下圖說明了參數iovcnt、iov及其所指數組與這兩個函數的關系。
writev()依次将iov[0]、iov[1]、...、 iov[iovcnt–1]指定的存儲區中的資料寫至fd指定的檔案。writev()的傳回值是寫出的資料總位元組數,正常情況下它應當等于所有資料塊長度之和。
readv()則将fd指定檔案中的資料按iov[0]、iov[1]、...、iov[iovcnt–1]規定的順序和長度,分散地讀到它們指定的存儲位址中。readv()的傳回值是讀入的總位元組數。如果沒有資料可讀和遇到了檔案尾,其傳回值為0。
有了這兩個函數,當想要集中寫出某張連結清單時,隻需讓iov數組的各個元素包含連結清單中各個表項的位址和其長度,然後将iov和它的元素個數作為參數傳遞給writev(),這些資料便可一次寫出。
4.send()-recv()函數
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv 和send的前3個參數等同于read和write。flags參數值為0或下面清單所示的選項:
flags | 說明 | recv | send |
MSG_DONTROUTE | 繞過路由表查找 | • | |
MSG_DONTWAIT | 僅本操作非阻塞 | • | • |
MSG_OOB | 發送或接收帶外資料 | • | • |
MSG_PEEK | 窺看外來消息 | • | |
MSG_WAITALL | 等待所有資料 | • |
4.1.send()函數說明
sockfd:指定發送端套接字描述符
buf: 存放要發送資料的緩沖區
len: 實際要發送資料的位元組數
flags: 一般設定為0
1) send先比較發送資料的長度len和套接字sockfd的發送緩沖區的長度。 如果len > 套接字sockfd的發送緩沖區的長度,該函數傳回SOCKET_ERROR;
2) 如果len <= 套接字sockfd的發送緩沖區的長度,那麼send先檢查協定是否正在發送sockfd的發送緩沖區中的資料。如果是就等待協定把資料發送完,如果協定還沒有開始發送sockfd的發送緩沖區中的資料或者sockfd的發送緩沖區中沒有資料,那麼send就比較sockfd的發送緩沖區的剩餘空間和len;
3) 如果len > 套接字sockfd的發送緩沖區剩餘空間的長度,send就一起等待協定把套接字sockfd的發送緩沖區中的資料發送完;
4) 如果len < 套接字sockfd的發送緩沖區剩餘空間大小,send就僅僅把buf中的資料copy到剩餘空間裡(注意并不是send把套接字sockfd的發送緩沖區中的資料傳輸到連接配接的另一端的,而是協定傳送的,send僅僅是把buf中的資料copy到套接字sockfd的發送緩沖區的剩餘空間裡);
5) 如果send函數copy成功,就傳回實際copy的位元組數,如果send在copy資料時出現錯誤,那麼send就傳回SOCKET_ERROR;如果在等待協定傳送資料時網絡斷開,send函數也傳回SOCKET_ERROR。
6) send函數把buf中的資料成功copy到sockfd的發送緩沖區的剩餘空間後它就傳回了,但是此時這些資料并不一定馬上被傳輸到連接配接的另一端。如果協定在後續的傳送過程中出現網絡錯誤的話,那麼下一個socket函數就會傳回SOCKET_ERROR(每一個除send的socket函數在執行的最開始總要先等待套接字的發送緩沖區中的資料被協定傳輸完畢才能繼續,如果在等待時出現網絡錯誤那麼該socket函數就傳回SOCKET_ERROR)。
7) 在unix系統下,如果send在等待協定傳送資料時網絡斷開,調用send的程序會接收到一個SIGPIPE信号,程序對該信号的處理是程序終止。
4.2.recv()函數說明
sockfd: 接收端套接字描述符
buf: 用來存放recv函數接收到的資料的緩沖區
len: 指明buf的長度
flags: 一般置為0
1) recv先等待sockfd的發送緩沖區的資料被協定傳送完畢,如果協定在傳送sockfd的發送緩沖區中的資料時出現網絡錯誤,那麼recv函數傳回SOCKET_ERROR
2) 如果套接字sockfd的發送緩沖區中沒有資料或者資料被協定成功發送完畢後,recv先檢查套接字sockfd的接收緩沖區,如果sockfd的接收緩沖區中沒有資料或者協定正在接收資料,那麼recv就一起等待,直到把資料接收完畢。當協定把資料接收完畢,recv函數就把sockfd的接收緩沖區中的資料copy到buf中(注意:協定接收到的資料可能大于buf的長度,是以在這種情況下要調用幾次recv函數才能把sockfd的接收緩沖區中的資料copy完。recv函數僅僅是copy資料,真正的接收資料是協定來完成的)。
3) recv函數傳回其實際copy的位元組數,如果recv在copy時出錯,那麼它傳回SOCKET_ERROR。如果recv函數在等待協定接收資料時網絡中斷了,那麼它傳回0。
4) 在unix系統下,如果recv函數在等待協定接收資料時網絡斷開了,那麼調用 recv的程序會接收到一個SIGPIPE信号,程序對該信号的預設處理是程序終止。
5.sendmsg()-recvmsg()函數
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
struct msghdr
{
void *msg_name; /* 消息的協定位址 */
socklen_t msg_namelen; /* 位址的長度 */
struct iovec *msg_iov; /* 多io緩沖區的位址 */
size_t msg_iovlen; /* 緩沖區的個數 */
void *msg_control; /* 輔助資料的位址 */
socklen_t msg_controllen; /* 輔助資料的長度 */
int msg_flags; /* 接收消息的辨別 */
};
參數sockfd:檔案描述符
參數msg:存放消息頭的記憶體緩沖
參數flags:是以下0個或者多個标志的組合體,可通過or操作連在一起。
MSG_DONTROUTE:不要使用網關來發送封包,隻發送到直接聯網的主機。這個标志主要用于診斷或者路由程式。
MSG_DONTWAIT:操作不會被阻塞。
MSG_EOR:終止一個記錄。
MSG_MORE:調用者有更多的資料需要發送。
MSG_NOSIGNAL:當另一端終止連接配接時,請求在基于流的錯誤套接字上不要發送SIGPIPE信号。
MSG_OOB:發送out-of-band資料(需要優先處理的資料),同時現行協定必須支援此種操作。
sendmsg和recvmsg這兩個接口是進階套接口,這兩個接口支援一般資料的發送和接收,還支援多緩沖區的封包發送和接收(readv和sendv也支援多緩沖區發送和接收),還可以在封包中帶輔助資料。這些功能是常用的send、recv等接口無法完成的。
關于這兩個函數的具體用法,可以參考本系列的後續文章。
6.sendto()-recvfrom()函數
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
這兩個函數一般用于UDP協定中。但是如果在TCP中connect函數調用後也可以使用。
6.1.sendto()函數說明
在無連接配接的資料報socket方式下,由于本地socket并沒有與遠端機器建立連接配接,是以在發送資料時應指明目的位址,sendto函數比send函數多了兩個參數,dest_addr表示目地機的IP位址和端口号資訊,而addrlen常常被指派為sizeof(struct sockaddr)。sendto()函數傳回值為實際發送的資料位元組長度或在出現發送錯誤時傳回-1。
sendto()函數主要用于SOCK_DGRAM類型套接口向dest_addr參數指定端的套接口發送資料報。對于SOCK_STREAM類型套接口,dest_addr和addrlen參數會被忽略;這種情況下sendto()等價于send()。
對于資料報類套接口,必需注意發送資料長度不應超過通訊子網的IP包最大長度。如果資料太長無法自動通過下層協定,則傳回EMSGSIZE錯誤,資料不會被發送。
另外需要注意的是,成功地完成sendto()調用并不意味着資料傳送到達。
如果發送端的緩沖區空間不夠儲存需傳送的資料,除非套接口處于非阻塞I/O方式,否則sendto()将阻塞。對于非阻塞SOCK_STREAM類型的套接口,實際寫的資料數目可能在1到所需大小之間,其值取決于本地和遠端主機的緩沖區大小。可用select()調用來确定何時能夠進一步發送資料。
6.2.recvfrom()函數說明
Recvfrom()函數傳回接收到的位元組數或當出現錯誤時傳回-1,并置相應的errno。
對于SOCK_STREAM類型的套接口,最多可接收緩沖區大小個資料。如果套接口被設定為線内接收帶外資料(選項為SO_OOBINLINE),且有帶外資料未讀入,則傳回帶外資料。應用程式可通過調用ioctlsocket()的SOCATMARK指令來确定是否有帶外資料待讀入。對于SOCK_STREAM類型套接口,忽略from和fromlen參數。
對于SOCK_DGRAM類型的套接口,隊列中第一個資料報中的資料被解包,但最多不超過緩沖區的大小。如果資料報大于緩沖區,那麼緩沖區中隻有資料報的前面部分,其他的資料都丢失了,并且recvfrom()函數傳回EMSGSIZE錯誤。
若from非零,且套接口為SOCK_DGRAM類型,則發送資料源的位址被複制到相應的sockaddr結構中。fromlen所指向的值初始化時為這個結構的大小,當調用傳回時按實際位址所占的空間進行修改。
如果沒有資料待讀,那麼除非是非阻塞模式,不然的話套接口将一直等待資料的到來,此時将傳回SOCKET_ERROR錯誤,錯誤代碼是EWOULDBLOCK。用select()或poll()可以獲知何時資料到達。
如果套接口為SOCK_STREAM類型,并且遠端“優雅”地中止了連接配接,那麼recvfrom()一個資料也不讀取,立即傳回。如果立即被強制中止,那麼recv()将以ECONNRESET錯誤失敗傳回。
在套接口的所設選項之上,還可用标志位flag來影響函數的執行方式。也就是說,本函數的語義既取決于套接口選項,也取決于标志位參數。标志位可取下列值:
MSG_PEEK ---- 檢視目前資料。資料将被複制到緩沖區中,但并不從輸入隊列中删除。
MSG_OOB ---- 處理帶外資料。
7.close()函數
TCP關閉連接配接有2種方式,一種是關閉端發送FIN,對方回應FIN+ACK,關閉端再回ACK,這是優雅的關閉連接配接。雙方可以保證所有資料都發送接收完成了。另一種是硬關閉,關閉方直接發送RSET,對方收到後立刻斷開連接配接。
#include <unistd.h>
int close(int fd);
成功傳回0,錯誤傳回-1。錯誤碼errno:EBADF表示fd不是一個有效描述符;EINTR表示close函數被信号中斷;EIO表示一個IO錯誤。
close函數首先将socket fd的reference減一,若reference依舊大于0,則該socket端口的狀态保持不變;若reference等于0,則首先将sender buffer中的資料全部發送出去,并将receive buffer中的資料全部丢棄,最後發送FIN,執行主動關閉。這裡的關閉是将讀寫兩個方向的資料傳輸全部關閉,是以在調用close之後并不能從中讀取到資料。
當多個程序共享一個套接字fd時,close每被調用一次,計數減1,直到計數為0時,也就是所有程序都調用了close()後,套接字才會被釋放。
windows平台上,對應的函數是closesocket(),分為下面3中情況:
1.close(l_onoff=0 預設狀态):
在套接口上不能再發出發送或接收請求。套接口發送緩沖區中的内容被發送到對端。如果描述字引用計數變為0,在發送完發送緩沖區中的資料後,觸發正常的TCP連接配接終止序列(發送FIN);套接口接收緩沖區中内容被丢棄。
這種方式下,就是在closesocket的時候立刻傳回,底層會将未發送完的資料發送完成後再釋放資源,也就是優雅的退出。
2.close(l_onoff = 1, l_linger =0):
在套接口上不能再發出發送或接受請求。如果描述子引用計數變為0,RST被發送到對端;連接配接的狀态被置為CLOSED(沒有TIME_WAIT狀态),套接口發送緩沖區和套接口接受緩沖區的資料被丢棄。
這種方式下,在調用closesocket的時候同樣會立刻傳回,但不會發送未發送完成的資料,而是通過一個REST包強制的關閉socket描述符,也就是強制的退出。
3.close(l_onoff =1, l_linger != 0):
在套接口上不能在發出發送或接收請求。套接口發送緩沖區中的内容被發送到對端。如果描述字引用計數變為0,在發送完發送緩沖區中的資料後,觸發正常的TCP連接配接終止序列(發送FIN);套接口接收緩沖區中内容被丢棄。如果在連接配接變成CLOSED狀态前延滞時間到,那麼close傳回EWOULDBLOCK錯誤。
這種方式下,在調用closesocket的時候不會立刻傳回,核心會延遲一段時間,這個時間就由l_linger得值來決定。如果逾時時間到達之前,發送完未發送的資料(包括FIN包)并得到另一端的确認,closesocket會傳回正确,socket描述符優雅性退出。否則,closesocket會直接傳回錯誤值,未發送資料丢失,socket描述符被強制性退出。需要注意的時,如果socket描述符被設定為非阻塞型,則closesocket會直接傳回值。
8.shutdown()函數
#include <sys/socket.h>
int shutdown(int sockfd, int how);
how的方式有三種分别是:
SHUT_RD(0):關閉sockfd上的讀功能,此選項将不允許sockfd進行讀操作。也就是該套接字不再接收資料,任何目前在套接字接收緩沖區的資料将被丢棄。程序将不能對該套接字發出任何讀操作。對TCP套接字該調用之後接收到的任何資料将被确認然後無聲的丢棄掉。但是,程序仍可往套接口發送資料,此方式對套接口發送緩沖區無影響。
SHUT_WR(1):關閉sockfd的寫功能,此選項将不允許sockfd進行寫操作。但是,程序仍可以從套接口接收資料,此方式對套接口接收緩沖區無影響。
SHUT_RDWR(2):關閉sockfd的讀寫功能。相當于調用shutdown兩次:首先是以SHUT_RD,然後以SHUT_WR。
傳回值:
成功則傳回0,錯誤傳回-1,錯誤碼errno:EBADF表示sockfd不是一個有效描述符;ENOTCONN表示sockfd未連接配接;ENOTSOCK表示sockfd是一個檔案描述符而不是socket描述符。
shutdown有2個作用。首先禁止後續的send或者recv,但注意它不會影響底層,也就是說,此前發出的異步send/recv不會傳回。其次,在所有發送的包被對方确認後,會發送FIN包給對方,試圖優雅的關閉連接配接。但shutdown本身并不影響任何底層的東西,是以,shutdown并且優雅關閉了連接配接之後,該socket以及其所關聯的資源依然存在,必須調用closesocket才能釋放。
shutdown函數禁止了這個套接字的上資料的發送以及接收,并且會發送FIN封包。特别說明的是:shutdown函數并不會去關閉fd,這個函數不會對fd進行操作。
9.close與shutdown差別
1).close - 關閉本程序的socket id,但連結還是開着的,用這個socket id的其它程序還能用這個連結,能讀或寫這個socket id。
shutdown - 則破壞了socket 連結,讀的時候可能偵測到EOF結束符,寫的時候可能會收到一個SIGPIPE信号,這個信号可能直到socket buffer被填充了才收到,shutdown還有一個關閉方式的參數,0 不能再讀,1不能再寫,2 讀寫都不能。
2).close - 中止一個連接配接,但它隻是減少描述符的參考數,并不直接關閉連接配接,隻有當描述符的參考數為0時才關閉連接配接。
shutdown - 可直接關閉描述符,不考慮描述符的參考數,可選擇中止一個方向的連接配接。
3).close - 如果多個程序共享一個套接字,close每被調用一次,計數減1,直到計數為0時,也就是所用程序都調用了close,套接字将被釋放。
shutdown - 如果多個程序共享一個套接字,其中某個程序shutdown(sfd, SHUT_RDWR)後,則其它的程序将無法進行通信。