什么是socket(套接字)
socket是一種計(jì)算機(jī)間約定好的傳輸方式(有點(diǎn)抽象)金抡。其本身為一串?dāng)?shù)字已球,unix將一切視為文件巍沙。每個(gè)文件都有自己的文件標(biāo)識(shí)符(一串?dāng)?shù)字)犯祠,那么網(wǎng)絡(luò)連接也是一個(gè)文件,他的文件標(biāo)識(shí)符就是socket窖铡。
-
IP
IPv4:一個(gè)32位整型數(shù)疗锐,4個(gè)字節(jié),表現(xiàn)為點(diǎn)分十進(jìn)制字符串费彼,eg:
192.168.247.135
IPv6:一個(gè)128位整形滑臊,十六個(gè)字節(jié),分成八份箍铲。每份兩個(gè)字節(jié)雇卷,用十六進(jìn)制表示,0000-ffff。eg:
2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b
-
如何查看本機(jī)IP关划?
打開CMD膘融,輸入
ipconfig
。
-
端口
- 端口的作用是定位到主機(jī)上的某一個(gè)進(jìn)程祭玉。
- 一個(gè)
unsigned short
氧映,十六位,0~65535
網(wǎng)絡(luò)分層模型OSI/ISO
分若干層脱货,長(zhǎng)這樣:
TCP岛都、UDP屬于傳輸層協(xié)議,IPv4振峻、IPv6屬于網(wǎng)絡(luò)層協(xié)議臼疫。
- 我們只需處理應(yīng)用層數(shù)據(jù),并指定傳輸層扣孟、網(wǎng)絡(luò)層所用協(xié)議即可烫堤。
大小端及其轉(zhuǎn)換
-
小端:主機(jī)字節(jié)序
- 數(shù)據(jù)的低位字節(jié)存儲(chǔ)到內(nèi)存的低地址位 , 數(shù)據(jù)的高位字節(jié)存儲(chǔ)到內(nèi)存的高地址位。
- 0x12345678 ---->> 78 56 34 12 內(nèi)存地址低—>高
-
大端:網(wǎng)絡(luò)字節(jié)序
- 據(jù)的低位字節(jié)存儲(chǔ)到內(nèi)存的高地址位 , 數(shù)據(jù)的高位字節(jié)存儲(chǔ)到內(nèi)存的低地址位凤价。
- 0x12345678 ---->> 12 34 56 78 內(nèi)存地址低—>高
// 有一個(gè)16進(jìn)制的數(shù), 有32位 (int): 0xab5c01ff // 字節(jié)序, 最小的單位: char 字節(jié), int 有4個(gè)字節(jié), 需要將其拆分為4份 // 一個(gè)字節(jié) unsigned char, 最大值是 255(十進(jìn)制) ==> ff(16進(jìn)制) 內(nèi)存低地址位 內(nèi)存的高地址位 ---------------------------------------------------------------------------> 小端: 0xff 0x01 0x5c 0xab 大端: 0xab 0x5c 0x01 0xff
-
轉(zhuǎn)換:
- 一般轉(zhuǎn)換
一般pc機(jī)用小端方式儲(chǔ)存數(shù)據(jù)鸽斟,那么我們?cè)诎l(fā)送數(shù)據(jù)前,就要將數(shù)據(jù)從小端轉(zhuǎn)換為大端利诺。接收到數(shù)據(jù)后富蓄,又要從大端轉(zhuǎn)換為小端。
#include <arpa/inet.h> // u:unsigned // 16: 16位, 32:32位 // h: host, 主機(jī)字節(jié)序 // n: net, 網(wǎng)絡(luò)字節(jié)序 // s: short // l: int // 這套api主要用于 網(wǎng)絡(luò)通信過(guò)程中 IP 和 端口 的 轉(zhuǎn)換 // 將一個(gè)短整形從主機(jī)字節(jié)序 -> 網(wǎng)絡(luò)字節(jié)序 uint16_t htons(uint16_t hostshort); // 將一個(gè)整形從主機(jī)字節(jié)序 -> 網(wǎng)絡(luò)字節(jié)序 uint32_t htonl(uint32_t hostlong); // 將一個(gè)短整形從網(wǎng)絡(luò)字節(jié)序 -> 主機(jī)字節(jié)序 uint16_t ntohs(uint16_t netshort) // 將一個(gè)整形從網(wǎng)絡(luò)字節(jié)序 -> 主機(jī)字節(jié)序 uint32_t ntohl(uint32_t netlong);
- IP地址轉(zhuǎn)換
IP地址雖為整形(int慢逾,4字節(jié))立倍,但實(shí)際以字符串來(lái)表示。以下函數(shù)可將字符串類型的IP地址進(jìn)行大小端轉(zhuǎn)換侣滩。
// 主機(jī)字節(jié)序的IP地址轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序 // 主機(jī)字節(jié)序的IP地址是字符串, 網(wǎng)絡(luò)字節(jié)序IP地址是整形 int inet_pton(int af, const char *src, void *dst);
af:
AF_INET: ipv4 格式的 ip 地址口注,
AF_INET6: ipv6 格式的 ip 地址
src:
要轉(zhuǎn)換的ip地址:102.168.0.1
dst:
一個(gè)指針,轉(zhuǎn)換得到的大端整形IP地址被放入這塊內(nèi)存君珠。
#include <arpa/inet.h> // 將大端的整形數(shù), 轉(zhuǎn)換為小端的點(diǎn)分十進(jìn)制的IP地址 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
與上相似寝志,size為dst內(nèi)存的大小。
TCP流程
Server:
-
建立用于監(jiān)聽的套接字:
SOCKET socket(int af, int type, int protocol);
af:AF_INET or AF_INET6葛躏,代表IPV4澈段、IPV6(PF一樣)
type:SOCK_STREAM or SOCK_DGRAM,前者對(duì)應(yīng)TCP舰攒,后者對(duì)應(yīng)UDP
protocol:IPPROTO_TCP or IPPTOTO_UDP,也可賦0悔醋,表示根據(jù)前兩個(gè)參數(shù)自行選取摩窃。
-
將監(jiān)聽套接字與本機(jī)的IP、端口綁定。(填寫自己的地址)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)猾愿;
參數(shù):
sockfd:上一步得到的用于監(jiān)聽的描述符
*addr:一個(gè)指向sockaddr類型結(jié)構(gòu)體的指針鹦聪,其中存儲(chǔ)著本地的IP地址以及端口。
-
sockaddr結(jié)構(gòu)體長(zhǎng)這樣:
struct sockaddr{ sa_family_t sin_family; //地址族(Address Family)蒂秘,也就是地址類型泽本,16個(gè)字節(jié),AF_INET char sa_data[14]; //IP地址+端口號(hào),端口2字節(jié)姻僧,IP地址4字節(jié)规丽,空閑(0)8個(gè)字節(jié) };
第二項(xiàng)將IP與端口揉在一起,不好填撇贺。我們用sockaddr_in填
-
sockaddr_in結(jié)構(gòu)體長(zhǎng)這樣:
struct sockaddr_in{ sa_family_t sin_family; //地址族(Address Family)赌莺,也就是地址類型 uint16_t sin_port; //16位的端口號(hào) struct in_addr sin_addr; //32位IP地址 char sin_zero[8]; //不使用,一般用0填充 }; //其中松嘶,in_addr長(zhǎng)這樣: struct in_addr{ in_addr_t s_addr; //32位的IP地址艘狭。in_addr_t 在頭文件 <netinet/in.h> 中定義,等價(jià)于 unsigned long };
-
-
使用中翠订,先使用sockaddr_in初始化巢音,再將其轉(zhuǎn)換為sockaddr類型。如下例:
//創(chuàng)建套接字 int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //創(chuàng)建sockaddr_in結(jié)構(gòu)體變量 struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); //每個(gè)字節(jié)都用0填充 serv_addr.sin_family = AF_INET; //使用IPv4地址 serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址 serv_addr.sin_port = htons(1234); //端口 //將套接字和IP尽超、端口綁定 connect(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
addrlen:參數(shù)2所對(duì)應(yīng)結(jié)構(gòu)體內(nèi)存的大小港谊,由sizeof計(jì)算得出。
成功返回0橙弱,失敗返回-1歧寺。
-
設(shè)置監(jiān)聽,監(jiān)聽有無(wú)客戶端連接棘脐。
int listen(SOCKET sock, int backlog);
讓套接字進(jìn)入被動(dòng)監(jiān)聽狀態(tài)斜筐。
backlog :為請(qǐng)求隊(duì)列的最大長(zhǎng)度。
請(qǐng)求隊(duì)列:
當(dāng)套接字正在處理客戶端請(qǐng)求時(shí)蛀缝,如果有新的請(qǐng)求進(jìn)來(lái)顷链,套接字是沒法處理的,只能把它放進(jìn)緩沖區(qū)屈梁,待當(dāng)前請(qǐng)求處理完畢后嗤练,再?gòu)木彌_區(qū)中讀取出來(lái)處理。如果不斷有新的請(qǐng)求進(jìn)來(lái)在讶,它們就按照先后順序在緩沖區(qū)中排隊(duì)煞抬,直到緩沖區(qū)滿。這個(gè)緩沖區(qū)构哺,就稱為請(qǐng)求隊(duì)列(Request Queue)革答。
緩沖區(qū)的長(zhǎng)度(能存放多少個(gè)客戶端請(qǐng)求)可以通過(guò) listen() 函數(shù)的 backlog 參數(shù)指定战坤,但究竟為多少并沒有什么標(biāo)準(zhǔn),可以根據(jù)你的需求來(lái)定残拐,并發(fā)量小的話可以是10或者20途茫。
如果將 backlog 的值設(shè)置為 SOMAXCONN,就由系統(tǒng)來(lái)決定請(qǐng)求隊(duì)列長(zhǎng)度溪食,這個(gè)值一般比較大囊卜,可能是幾百,或者更多错沃。
當(dāng)請(qǐng)求隊(duì)列滿時(shí)栅组,就不再接收新的請(qǐng)求,對(duì)于 Linux捎废,客戶端會(huì)收到 ECONNREFUSED 錯(cuò)誤笑窜,對(duì)于 Windows,客戶端會(huì)收到 WSAECONNREFUSED 錯(cuò)誤登疗。
注意:listen() 只是讓套接字處于監(jiān)聽狀態(tài)排截,并沒有接收請(qǐng)求。接收請(qǐng)求需要使用 accept() 函數(shù)辐益。
成功返回0断傲,失敗返回-1
-
等待客戶端連接請(qǐng)求,不來(lái)就阻塞智政,來(lái)了就建立新的連接认罩,得到新的用于通信的套接字
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);
參數(shù)同bind。
不同的是续捂,bind中sockaddr保存的是本機(jī)的IP與端口垦垂,而此函數(shù)中的sockaddr需要新建一個(gè)結(jié)構(gòu)體,不用初始化牙瓢,執(zhí)行函數(shù)時(shí)會(huì)自動(dòng)保存客戶端的IP地址與端口號(hào)劫拗。
accept() 返回一個(gè)新的套接字來(lái)和客戶端通信,addr 保存了客戶端的IP地址和端口號(hào)矾克,而 sock 是服務(wù)器端的套接字页慷。后面和客戶端通信時(shí),要使用這個(gè)新生成的套接字胁附,而不是原來(lái)服務(wù)器端的套接字酒繁。
最后需要說(shuō)明的是:listen() 只是讓套接字進(jìn)入監(jiān)聽狀態(tài),并沒有真正接收客戶端請(qǐng)求控妻,listen() 后面的代碼會(huì)繼續(xù)執(zhí)行州袒,直到遇到 accept()。accept() 會(huì)阻塞程序執(zhí)行(后面代碼不能被執(zhí)行)饼暑,直到有新的請(qǐng)求到來(lái)稳析。
-
收發(fā)文件
int send(SOCKET sock, const char *buf, int len, int flags);
sock:即用于通信的socket洗做,也就是accept等號(hào)左邊那玩意弓叛。
buf:指向一塊內(nèi)存彰居。注意,const只讀撰筷,也就是說(shuō)我們只讀這塊內(nèi)存的數(shù)據(jù)陈惰,不用寫。*
len:內(nèi)存的大小
flags:特殊的屬性毕籽,一般不使用抬闯,指定為0
返回:大于0-->實(shí)際發(fā)送的字節(jié)數(shù),等于0-->對(duì)方斷開連接关筒,-1-->接受失敗
int recv(SOCKET sock, char *buf, int len, int flags);
這里的*buf是可以寫的溶握。
-
關(guān)閉套接字
close(SOCKET sock)
Client:
-
創(chuàng)建用于通信的套接字
socket()
-
連接服務(wù)器,需要知道服務(wù)器的IP與端口
connect()
int connect(SOCKET sock, const struct sockaddr *addr, int addrlen);
參數(shù)與bind蒸播、accept一樣睡榆。
其中,sockaddr存的是本機(jī)(Client)的IP與端口袍榆,所以與bind完全一致胀屿,要初始化。
-
收發(fā)文件
send包雀、recv
-
關(guān)閉套接字
close()
[圖片上傳失敗...(image-a27a45-1648644517809)]
程序?qū)嵗?/h2>
服務(wù)器端代碼 server.cpp
#include <stdio.h>
#include <winsock2.h>
#pragma comment (1lib, "ws2_32.lib") //加載 ws2_32.dll
int main(){
//初始化 DLL
WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData);
//創(chuàng)建套接字
//PF_INET:IPV4地址宿崭,SOCK_STREAM:面向聯(lián)接的傳輸方式(另一種SOCK_DGRAM),IPPROTO_TCP:使用TCP協(xié)議
SOCKET servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//綁定套接字
sockaddr_in sockAddr; //sockaddr_in是包中定義好的結(jié)構(gòu)體
memset(&sockAddr, 0, sizeof(sockAddr)); //每個(gè)字節(jié)都用0填充才写,void* memset(起始指針,填充數(shù)值,內(nèi)存長(zhǎng)度)葡兑,給指定內(nèi)存賦值
sockAddr.sin_family = PF_INET; //使用IPv4地址
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
sockAddr.sin_port = htons(1234); //端口
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)); //將套接字servSock與特定IP地址與端口綁定。
//進(jìn)入監(jiān)聽狀態(tài)赞草,讓套接字處于被動(dòng)監(jiān)聽狀態(tài)讹堤。所謂被動(dòng)監(jiān)聽,是指套接字一直處于“睡眠”中房资,直到客戶端發(fā)起請(qǐng)求才會(huì)被“喚醒”
listen(servSock, 20);
//接收客戶端請(qǐng)求
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize); //accept() 函數(shù)用來(lái)接收客戶端的請(qǐng)求蜕劝。程序一旦執(zhí)行到 accept() 就會(huì)被阻塞(暫停運(yùn)行),直到客戶端發(fā)起請(qǐng)求轰异。
//向客戶端發(fā)送數(shù)據(jù)
char *str = "Hello World!";
send(clntSock, str, strlen(str)+sizeof(char), NULL);
//關(guān)閉套接字
closesocket(clntSock);
closesocket(servSock);
//終止 DLL 的使用
WSACleanup();
return 0;
}
客戶端代碼 client.cpp
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib") //加載 ws2_32.dll
int main(){
//初始化DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//創(chuàng)建套接字
SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//向服務(wù)器發(fā)起請(qǐng)求
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每個(gè)字節(jié)都用0填充
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(1234);
connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)); // connect() 向服務(wù)器發(fā)起請(qǐng)求岖沛,直到服務(wù)器傳回?cái)?shù)據(jù)后,connect() 才運(yùn)行結(jié)束搭独。
//接收服務(wù)器傳回的數(shù)據(jù)
char szBuffer[MAXBYTE] = {0};
recv(sock, szBuffer, MAXBYTE, NULL);
//輸出接收到的數(shù)據(jù)
printf("Message form server: %s\n", szBuffer);
//關(guān)閉套接字
closesocket(sock);
//終止使用 DLL
WSACleanup();
system("pause");
return 0;
}
WSAStartup()
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
初始化婴削。
wVersionRequested 為 WinSock 規(guī)范的版本號(hào),低字節(jié)為主版本號(hào)牙肝,高字節(jié)為副版本號(hào)(修正版本號(hào))唉俗;WinSock 規(guī)范的最新版本號(hào)為 2.2嗤朴。
lpWSAData 為指向 WSAData 結(jié)構(gòu)體的指針。
具體實(shí)例:
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData); //2.2