網(wǎng)絡編程離不開socket,小猿圈這篇詳解一下socket創(chuàng)建,仔細學完這篇對你認識網(wǎng)絡底層的東西有著很重要的作用,同時即便有已經(jīng)寫好的模塊用,但是這個東西一定要掌握的,大家一定要認真看。
TCP通信
一個程序使用套接字需要執(zhí)行4個步驟。
--分配套接口和初始化
--連接
--發(fā)送或接收數(shù)據(jù)
--關閉套接字
涉及到的調(diào)用包括socket、bind、listen、connect(阻塞線程)、accept(阻塞線程)、recv(阻塞線程)、send(阻塞線程)。
分配套接口和初始化
--我們需要做的第一件工作就是分配套接口。
--套接口可以看作是文件描述符
--不論server端,還是client端,第一步都是一樣的
每個套接口都是一個通信的通道
兩個進程通過套接口建立連接后就可以發(fā)送和接收數(shù)據(jù)了。
socket()
int socket(int domain.int tyoe,int protocol);
系統(tǒng)調(diào)用socket帶有以下參數(shù)
--int domain
--int tyoe
--int protocol(這個值一般都取0)
--成功返回套接字描述符,失敗返回-1,并設置errno
socket參數(shù)
domain說明
AF_UNIX? ? ? ? UNIX內(nèi)部使用
AF_INET? ? ? ? TCP/IP協(xié)議
AF_ISO? ? ? ? 國際標準組織協(xié)議
AF_NS? ? ? ? Xerox網(wǎng)絡協(xié)議
type說明
SOCK_STREAM? ? 使用TCP可靠連接
SOCK_DGRAM? ? 使用UDP不可靠連接
bind()函數(shù)
int bind(int sockfd,const struct sockaddr *my_addr,socklen_t addrlen);
bind將進程和一個套接口聯(lián)系起來,bind通常用于服務器進程為接入客戶連接建立一個套接口(簡單而言,就是把程序和一個IP地址端口號綁定在一起)。
參數(shù)sockfd是函數(shù)socket調(diào)用返回的套接口。
參數(shù)my_addr是結構sockaddr的地址(用來描述IP地址的一個結構)。
參數(shù)addrlen設置了my_addr能容納的最大字節(jié)數(shù)。
成功返回0,失敗返回-1,并設置errno。
一個端口號只能綁定一個程序,1對1關系
socklen_t本質(zhì)上是unsigned int,并不是int,在windows操作系統(tǒng)下才是int。
對于客戶端程序,下一步是要與之通信的服務器建立連接。
--客戶端只需使用connect即可
對于服務端程序,就是要建立自己的套接口等待來自客戶端的連接。
--服務器需要調(diào)用listen和accept兩個函數(shù)。
listen()函數(shù)
int listen(int sockfd,int backlog);
創(chuàng)建了套接口并且使用bind將它和一個進程關聯(lián)起來以后,服務端就需要調(diào)用listen來監(jiān)聽指定端口的客戶端連接。
參數(shù)sockfd是調(diào)用socket返回的套接口描述符
參數(shù)backlog設置接入隊列的大小,通常把這個值設置的足夠大就可以了。
參數(shù)backlog一般用來設置服務端可以并發(fā)的接收客戶端的最大連接數(shù)目
listen也是從TCP緩存中讀取連接,并非來一個接收一個。
成功返回0,失敗返回-1,并設置errno
listen是將連接直接放到緩存區(qū)里,等待accept函數(shù)來接收
accept()函數(shù)
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
當有客戶端連接到服務端,他們會排入隊列,知道服務端準備好處理他們位置為止,accept會返回一個新的套接口,同時原來的套接口繼續(xù)listen指定端口號
參數(shù)sockfd是調(diào)用socket返回的套接口描述符
參數(shù)addr指向結構sockaddr地址,表示客戶端的IP地址。
參數(shù)addrlen設置了addr能容納的最大字節(jié)數(shù)。
成功返回新的套接字,失敗返回-1,并設置errno
connect()函數(shù)
int connect(int sockfd,const struct sockaddr * serv_addr,socklen_t addrlen);
客戶端調(diào)用connect與服務端進行連接。
參數(shù)sockfd是調(diào)用socket返回的套接口描述符。
參數(shù)addr指向結構sockaddr地址。
參數(shù)addrlen設置了addr能容納的最大字節(jié)數(shù)。
成功返回0,失敗返回-1,并設置errno。
connect()函數(shù)也是阻塞的,它必須完成三次握手機制才能返回。
客戶端和服務端建立了連接就可以在客戶端和服務端之間傳輸數(shù)據(jù)了,需要兩個系統(tǒng)調(diào)用。
--send? ? ? ? 發(fā)送數(shù)據(jù)。
--recv? ? ? ? 接收數(shù)據(jù)。
一個套接口既可以發(fā)送數(shù)據(jù),也可以接收數(shù)據(jù),網(wǎng)絡是一個雙向管道。
send()函數(shù)
ssize_t send(int s,const void *buf,size_t len,int flags);
send函數(shù)用來發(fā)送數(shù)據(jù)。
參數(shù)s是已經(jīng)建立連接的套接口。
參數(shù)buf是發(fā)送數(shù)據(jù)內(nèi)存buffer地址指針。
參數(shù)len指明buffer的大小,單位字節(jié)。
參數(shù)flags一般填0.
成功返回發(fā)送的字節(jié)數(shù),失敗返回-1,并設置errno。
send()函數(shù)注意點
1.send返回值理解
? ? send在阻塞場景下,返回值要么是指定長度(發(fā)送成功),要么是-1,發(fā)送失敗,但是在非阻塞場景下,
返回值可能小于指定長度,這是因為發(fā)送數(shù)據(jù)超過發(fā)送緩沖區(qū)(窗口),所以只能發(fā)送緩沖區(qū)大小的數(shù)據(jù),剩下的數(shù)據(jù)無法發(fā)送
2.errno=11的理解
? ? send在非阻塞場景可能返回-1,并且更新errno為11,11表示資源臨時不可用,當發(fā)送緩沖區(qū)滿了,
而程序不斷在調(diào)用send(0函數(shù)發(fā)送數(shù)據(jù)就會出現(xiàn)這個錯誤,此時收到返回值為-1,并且errno=11時,需要停止發(fā)送數(shù)據(jù),等待套接字下次可寫的時候再發(fā)送數(shù)據(jù)。
recv()函數(shù)
ssize_t recv(int s,void *buf,size_t len,int flags);
recv函數(shù)用來接收數(shù)據(jù)。
參數(shù)s是已經(jīng)建立連接的套接口。
參數(shù)buf是接收數(shù)據(jù)內(nèi)存buffer地址指針。
參數(shù)len指明buffer的大小,單位字節(jié)。
參數(shù)flags一般填0.
成功返回接收到的字節(jié)數(shù),失敗返回-1,并且設置errno,如果對端套接字已經(jīng)關閉,返回0.
recv函數(shù)只是從TCP緩存中讀數(shù)據(jù)(此時數(shù)據(jù)已經(jīng)在自己的電腦上了),不是直接從網(wǎng)絡中讀數(shù)據(jù),什么時候TCP緩存區(qū)滿了,另一邊的send函數(shù)才會停止發(fā)送數(shù)據(jù)。
recv()函數(shù)會阻塞線程,直到收到消息或者客戶端關閉。
最后當你用完套接口以后,就該釋放套接口所占用的資源了,通過close做到這一點
當試圖向一個已經(jīng)關閉的套接口寫或者讀數(shù)據(jù)就會出錯。
setsockopt()函數(shù)
int setsockopt(int s,int level,int optname,const void *optval,socklen optlen);
setsockopt函數(shù)設置套接口
函數(shù)成功返回0,失敗返回-1,并設置errno
常見用法為:
int on=1;
setsockopt(st,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
由于TCP套接字狀態(tài)TIME_WAIT引起該套接字關閉后約保留2到4分鐘。在此期間bind綁定該端口失敗。
SO_REUSEADDR指示系統(tǒng)地址可重用。
當服務器在listen()函數(shù)后直接退出,如果這時候有一個客戶端來連接,這個連接會被放在緩存中,如果這時候啟動了一個新的程序綁定這個IP地址和端口號,
那么這新的程序就會接收到上一個客戶端的請求,這是有問題,因為上一個客戶端請求訪問的是原始程序。但是如果我們就一直一個程序綁定這個IP地址,就用不著這個TIME_WAIT信號了,
我們服務器上就一個server綁定這個IP地址和端口號,所以用不著這個機制,因此調(diào)用setsockopt()函數(shù)。
IP地址的結構
ip地址在內(nèi)存中用int表示,int在內(nèi)存中占有4個字節(jié)的空間
第一個字節(jié):192
第二個字節(jié):168
第三個字節(jié):1
第四個字節(jié):2
send()、recv()函數(shù)原理解析
? 通過上一章網(wǎng)絡編程一中的主機之間的通訊圖可以得知,兩主機之間通信,主機A先將消息打包成TCP包,TCP包再打包成IP包,然后形成以太網(wǎng)包發(fā)送,
另一臺主機的接收順序恰好相反,先接受以太網(wǎng)包,再接收IP包,最后接收TCP包,通過程序中測試,recv()函數(shù)一次能夠接受的數(shù)據(jù)(red hat中最大能接收64K數(shù)據(jù))要
比send()函數(shù)發(fā)送的數(shù)據(jù)(red hat中最大發(fā)送的數(shù)據(jù)超過128K),send()函數(shù)是將數(shù)據(jù)打包成TCP包再發(fā)送,而接收數(shù)據(jù)的時候是接收的IP包,再將IP包還原成TCP包,
這說明TCP包的容量實際上會大于或者等于IP包,事實上計算機在發(fā)送數(shù)據(jù)時,如果TCP包過大,會把TCP拆解成多個IP包,并將這些IP包存儲在網(wǎng)卡的緩存區(qū)里(如果send
發(fā)送數(shù)據(jù)超過緩沖區(qū),那么sned()所在的線程就會被掛起),recv在網(wǎng)卡上也有緩存區(qū),網(wǎng)絡發(fā)送過來的數(shù)據(jù)會先存放在緩存區(qū)中,直到緩存區(qū)被填滿,此時發(fā)送方就會停
止發(fā)送,但是此時不意味這send函數(shù)所在的線程就會被掛起,send函數(shù)還可以往自己的緩存區(qū)中發(fā)數(shù)據(jù),直到緩存區(qū)填滿。
在linux進行非阻塞的socket接收數(shù)據(jù)時經(jīng)常出現(xiàn)Resource temporarily unavailable,errno代碼為11(EAGAIN),
這表明你在非阻塞模式下調(diào)用了阻塞操作,在該操作沒有完成就返回這個錯誤,
這個錯誤不會破壞socket的同步,不用管它,下次循環(huán)接著recv就可以。
對非阻塞socket而言,EAGAIN不是一種錯誤。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。
另外,如果出現(xiàn)EINTR即errno為4,錯誤描述Interrupted system call,該錯誤是由于操作被信號打算,操作也應該繼續(xù)。
最后,如果recv的返回值為0,那表明連接已經(jīng)斷開,我們的接收操作也應該結束。
看到這對大概流程以及概念的東西有了一個深刻了解吧,下一節(jié)小編可以詳細描述一下里面的運作過程,就是通信過程,一接一收,如果有點疑惑的朋友可以去小猿圈尋找一下答案,看懂的朋友可以期待下一次小猿圈的更新!