客戶端socket簡單示例(以下都是針對TCP協(xié)議)
#define BUF_SIZE 100
int socket = socket(AF_INET,SOCK_STREAm,IPPROTO_TCP);
struct sockaddr_in sockAddr;
sockAddr.sn_famil = AF_INET;
sockAddr.sn_addr.s_addr = inet_addr(127.0.0.1);//處理網(wǎng)絡(luò)字節(jié)序憔晒,大小端差異
sockAddr.sn_port = htons(1234);
connect(socket,(struct sockAddr *)&sockAddr, sizeof(sockAddr) );
char buffer[BUF_SIZE];
read(socket,buffer,sizeof(BUF_SIZE)-1);
close(socket);
服務(wù)端sockt簡單示例
AF_INET:代表ipv4地址苍苞;SOCK_STREAM:數(shù)據(jù)傳輸方式颤专,代表面向連接的數(shù)據(jù)傳輸方法销斟;IPPROTO_TCP:指定協(xié)議
int socket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockAddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr));//清理內(nèi)存
sockAddr.sin_famil = AF_INET;
sockAddr.sin_addr.s_addr = inet_addr(127.0.0.1);
sockAddr.sin_port = htons(1234);
bind(socket,(struct sockAddr_in*)sockAddr,sizeof(sockAddr));//綁定socket描述符,把組合體特定地址分配給socket踢关。
listen(socket,20);//socket是對應(yīng)描述符咐鹤,20 是可以排隊的最大連接數(shù)
sickle_t clnt_addr_size = sizeof(clnt_addr);
int clinSocket = accpect(socket, (struct sockaddr*)&serv_addr,clnt_addr_size);//獲取客戶端的socket
char str[] = "你好,客戶端";
write(socket, str,sizeof(str));
close(clinSocket);
close(socket);
客戶端流程
- 客戶端創(chuàng)建socket侨核,先調(diào)socket函數(shù)創(chuàng)建個socket返回socket描述符草穆,它存在于協(xié)議族空間中沒有具體地址。
- 聲明結(jié)構(gòu)體sockAddr_in,賦值地址族或稱協(xié)議域搓译,IP地址悲柱,端口,用來識別主機(jī)中的進(jìn)程些己。
- conect連接服務(wù)器(這里開始三次握手連接)豌鸡;
- 然后可以使用下面函數(shù)進(jìn)行發(fā)送接收數(shù)據(jù)了
- read()/write()
- recv()/send()
- readv()/writev()
- recvmsg()/sendmsg()
- recvfrom()/sendto()
- close() 使用后可以關(guān)閉數(shù)據(jù)了(這里開始四次握手?jǐn)嚅_連接)。
服務(wù)端流程
- 客戶端創(chuàng)建socket段标,先調(diào)socket函數(shù)創(chuàng)建個socket返回socket描述符涯冠,它存在于協(xié)議族空間中沒有具體地址。
- 聲明結(jié)構(gòu)體sockAddr_in,賦值地址族或稱協(xié)議域逼庞,IP地址蛇更,端口,用來識別主機(jī)中的進(jìn)程。
- 調(diào)用bind()函數(shù)把一個地址族中的特定地址賦給socket派任,如:把AF_INET砸逊,SOCK_STREAM,IPPROTO_TCP組合賦值給socket掌逛。如果沒有調(diào)用過bind()這個函數(shù)师逸,客戶端調(diào)用connect() 或者服務(wù)端調(diào)用listen()監(jiān)聽函數(shù),系統(tǒng)會自動分配一個端口號和自身的IP地址組合豆混,用于提供服務(wù)篓像,而客戶端就不用指定,這也是服務(wù)端代碼要調(diào)bind()函數(shù)而客戶端不用的原因皿伺。(客戶端不用bind函數(shù))遗淳。
- 調(diào)用listen(),開始監(jiān)聽上面創(chuàng)造的socket。
- 當(dāng)socket處于listen狀態(tài),調(diào)用accept()程序就會進(jìn)入阻塞狀態(tài)心傀,直到接受到客戶端的connect()請求。accept() 會返回一個和客戶端有關(guān)的socket描述字符拆讯,然后可以對這個字符進(jìn)行各種讀寫操作脂男,便是向客戶端發(fā)送數(shù)據(jù)。
6.使用完成后便可以close關(guān)閉自己創(chuàng)建的socket和accept返回的socket
注:close是將socket從內(nèi)存中清楚种呐,還有種方式shutdown宰翅,用來斷開連接,但是不會清除socket爽室,函數(shù)原型int shutdown(int sock, int howto),howto參數(shù)表示斷開方式汁讼。在linux下有以下幾個方式。
- SHUT_RD:斷開輸入流阔墩。套接字無法接收數(shù)據(jù)(即使輸入緩沖區(qū)收到數(shù)據(jù)也被抹去)嘿架,無法調(diào)用輸入相關(guān)函數(shù)。其他平臺稍有不同啸箫。
- SHUT_WR:斷開輸出流耸彪。套接字無法發(fā)送數(shù)據(jù),但如果輸出緩沖區(qū)中還有未傳輸?shù)臄?shù)據(jù)忘苛,則將傳遞到目標(biāo)主機(jī)蝉娜。
- SHUT_RDWR:同時斷開 I/O 流。相當(dāng)于分兩次調(diào)用 shutdown()扎唾,其中一次以
- SHUT_RD 為參數(shù)召川,另一次以 SHUT_WR 為參數(shù)
三次握手建立連接
首先得有個服務(wù)端處于監(jiān)聽socket的狀態(tài),accept阻塞著線程等待客戶端的請求胸遇。
下圖是網(wǎng)上找的TCP數(shù)據(jù)報結(jié)構(gòu)荧呐,握手時的數(shù)據(jù)包格式
三次握手中,需要對TCP頭部的這幾個概念理解
- seq表示包的序號。
- ack來確認(rèn)對方發(fā)送的某個序號seq收到了坛增,采用ack = seq + 1的方式
- URG获雕,ACK,PSH收捣,PST届案,SYN,F(xiàn)IN這6個bit表示標(biāo)志罢艾,SYN表示是同步連接數(shù)據(jù)包楣颠,F(xiàn)IN是否斷連數(shù)據(jù)包。所以四次斷連發(fā)送的就是FIN包咐蚯。
第一次:客戶端調(diào)用connect()向目標(biāo)socket發(fā)起連接請求童漩,這一步客戶端socket會組織一個數(shù)據(jù)包比如:ACK標(biāo)志位不設(shè)置,設(shè)置SYN標(biāo)志位(Synchronous同步的意思春锋,同步建立連接)標(biāo)志位 =1矫膨,seq = x,進(jìn)入SYN-SEND(同步已發(fā)送)狀態(tài)期奔,發(fā)送給服務(wù)端侧馅,此時connect()函數(shù)會進(jìn)入阻塞狀態(tài),等待服務(wù)端的返回呐萌。
第二次:服務(wù)端接受到客戶端的請求和數(shù)據(jù)包后馁痴,組織數(shù)據(jù)包,SYN置1肺孤,ACK標(biāo)志位(Acknowledge確認(rèn)的意思)置1罗晕, 同時生成一個確認(rèn)序號ack= x+1 ,同時隨機(jī)選擇seq = y進(jìn)入SYN-RCVD(同步已發(fā)送)狀態(tài)赠堵。
第三次:客戶端接受到了服務(wù)端的確認(rèn)連接請求數(shù)據(jù)包小渊,此時connect會返回,進(jìn)入連接狀茫叭,組織數(shù)據(jù)包:SYN標(biāo)志位不設(shè)置粤铭,ACK置1,確認(rèn)號ack = y+1杂靶,seq= x + 1(我第二次發(fā)給你包)梆惯,再次發(fā)送給服務(wù)端,表示已經(jīng)知道你已經(jīng)收到我想連接的請求并且可以連接了吗垮。此時服務(wù)端收到數(shù)據(jù)后垛吗,進(jìn)入已建立連接的狀態(tài),accept()函數(shù)會返回一個socket 描述符烁登,即怯屉,我們在服務(wù)端進(jìn)行讀寫操作的socket蔚舀,這樣連接就完成了。
注:
seq的作用是標(biāo)識數(shù)據(jù)包序號锨络,ack的作用是確認(rèn)對方發(fā)送相應(yīng)seq序號的數(shù)據(jù)包是否收到赌躺,把客戶端發(fā)的seq加上1賦值給ack,然后返回給客戶端表示已經(jīng)收到你的連接請求羡儿,同時自己根據(jù)ack的值礼患,再次組織下一個按序號應(yīng)該發(fā)送的包,給seq賦值掠归,發(fā)送給客戶端缅叠,客戶端收到數(shù)據(jù)后,查看ack是否是自己第一次發(fā)送數(shù)據(jù)包里的seq加上1的值虏冻,來確認(rèn)服務(wù)端有沒有收到自己的連接請求肤粱,然后客戶端重新組織數(shù)據(jù)包,把服務(wù)端數(shù)據(jù)包里的seq加上1 賦值給自己的ack確認(rèn)號厨相,發(fā)送給服務(wù)端领曼,表示我已經(jīng)知道你收到我發(fā)送連接的請求了。
這個過程中若是收到服務(wù)的數(shù)據(jù)包ack并不是上次自己發(fā)送seq加上1蛮穿,則會認(rèn)為服務(wù)端沒有收到數(shù)據(jù)悯森,會重新組織數(shù)據(jù)再次發(fā)送,直到接到服務(wù)端包含正確的seq數(shù)據(jù)包或者等待數(shù)據(jù)包時間超時失效绪撵。
四次握手?jǐn)嚅_連接
客戶端調(diào)用close()函數(shù)后,向服務(wù)端發(fā)送FIN數(shù)據(jù)包(設(shè)置了FIN標(biāo)志位為1)祝蝠,客戶端進(jìn)入第一等待階段音诈。
服務(wù)器收到數(shù)據(jù)包后,檢測到設(shè)置FIN標(biāo)志位绎狭,知道要斷開連接了细溅,向客戶端發(fā)送一個確認(rèn)包,服務(wù)端進(jìn)入關(guān)閉等待階段儡嘶。并不是立即關(guān)閉喇聊,只是先向客戶端發(fā)送確認(rèn)包,告訴客戶端蹦狂,我知道要斷開了誓篱,準(zhǔn)備些事情。此時凯楔,客戶端收到確認(rèn)包后窜骄,等待服務(wù)器準(zhǔn)備完畢再次給自己發(fā)送數(shù)據(jù)包,這時客戶端進(jìn)入第二等待階段摆屯。
過一段時間后邻遏,服務(wù)器準(zhǔn)備完成,可以斷開連接了,于時再次發(fā)送FIN包准验,告訴客戶端赎线,準(zhǔn)備好了,可以斷開連接了糊饱,這時服務(wù)端進(jìn)入一個LAST_ACK的狀態(tài)(猜測時只能接收一個ACK包的狀態(tài))垂寥。
-
客戶端收到服務(wù)端的FIN數(shù)據(jù)包后,再次向服務(wù)端發(fā)送ACK包济似,告訴服務(wù)端矫废,斷開吧,就進(jìn)入了TIME_WAIT狀態(tài)(注意砰蠢,此時客戶端并不是立即斷開連接蓖扑,而是會等待一會,因為會存在數(shù)據(jù)包丟失的情況台舱,若是服務(wù)端沒有收到自己最后發(fā)送的數(shù)據(jù)包律杠,則服務(wù)端還會向客戶端發(fā)送FIN_2數(shù)據(jù)包,等待時間竞惋,與數(shù)據(jù)包在網(wǎng)絡(luò)中的有效時限有關(guān))柜去。此時,服務(wù)端接受到數(shù)據(jù)后會關(guān)閉Socket進(jìn)入Close狀態(tài)拆宛∩ど荩客戶端等了一段時間后仍沒有接到FIN_2的數(shù)據(jù)包,就會認(rèn)為浑厚,服務(wù)端已經(jīng)接到自己最后一次發(fā)送的數(shù)據(jù)包股耽,這時客戶端才會進(jìn)入CLOSE的狀態(tài)。
四次斷連圖
socket緩沖區(qū)以及阻塞模式
socket緩沖區(qū)
每個socket創(chuàng)建后钳幅,都會分配兩個緩沖區(qū)物蝙,輸入緩沖區(qū)和輸出緩沖區(qū)。
write()和send() 并不是立即向網(wǎng)絡(luò)中傳輸數(shù)據(jù)敢艰,而是先將數(shù)據(jù)寫入緩沖區(qū)中诬乞,由TCP協(xié)議將數(shù)據(jù)發(fā)送到目標(biāo)機(jī)器,一旦數(shù)據(jù)寫入緩沖區(qū)钠导,函數(shù)就可以成功返回震嫉,不管有沒有到達(dá)目標(biāo)機(jī)器,也不管他們何時被發(fā)送到網(wǎng)絡(luò)牡属,這些都是TCP負(fù)責(zé)的事情责掏。
這些I/O緩沖區(qū)特性如下:
- I/O緩沖區(qū)在每個TCP套接字中單獨(dú)存在
- I/O緩沖區(qū)在創(chuàng)建套接字時會自動生成
- 即使關(guān)閉套接字也會繼續(xù)傳送輸出緩沖區(qū)中遺留的數(shù)據(jù)。
-
關(guān)閉套接字將丟失輸入緩沖區(qū)中的數(shù)據(jù)
四次握手?jǐn)噙B示意圖.jpg
阻塞模式
對于TCP套接字湃望,當(dāng)使用write()/send()發(fā)送數(shù)據(jù)時:
- 首先會檢查緩沖區(qū)换衬,若緩沖區(qū)的剩余空間小雨要發(fā)送的數(shù)據(jù)時痰驱,write()/send()進(jìn)入阻塞模式,將會等待數(shù)據(jù)發(fā)送到目標(biāo)機(jī)器瞳浦,騰出足夠空間担映,才會喚醒write()/send()函數(shù)繼續(xù)寫入數(shù)據(jù)。
- 如果TCP協(xié)議正在向網(wǎng)絡(luò)發(fā)送數(shù)據(jù)叫潦,那么輸出緩沖區(qū)將會被鎖定蝇完,不允許寫入,write/send()函數(shù)寫入數(shù)據(jù)矗蕊。
- 如果要寫入的數(shù)據(jù)大于緩沖區(qū)的最大長度短蜕,那么將會分批寫入。
- 直到所有數(shù)據(jù)被寫入緩沖區(qū) write()/send() 才能返回傻咖。
當(dāng)使用read和recv讀取數(shù)據(jù)時朋魔。 - 手寫會檢查緩沖區(qū),如果緩沖區(qū)中有數(shù)據(jù)卿操,那么就讀取警检,否則函數(shù)會被阻塞,直到網(wǎng)絡(luò)上有數(shù)據(jù)到來害淤。
- 如果要讀取的數(shù)據(jù)長度小于緩沖區(qū)中的數(shù)據(jù)長度扇雕,那么就不能不一次性將緩沖區(qū)中的所有數(shù)據(jù)讀出,剩余數(shù)據(jù)將不斷積壓窥摄,直到read()/recv()函數(shù)才會返回镶奉,否則就一直被阻塞。
- 直到讀取到數(shù)據(jù)后 read()/recv()才會返回崭放,否則就一直被阻塞哨苛。
所謂阻塞,就是上一步動作沒有完成莹菱,下一步動作將暫停,直到上一步動作完成后才能繼續(xù)吱瘩,以保持同步性道伟。