-----初更----2018年1月26日(轉載請注明出處)----
前言:
昨天把socket基本的知識點總結了一下,今天就開始寫第一個socket程序吧,這篇文章中我們要做到的就是本機和本機通信,完成一次發送接收,雖然看着比較簡單,但是理解起來也需要費一番功夫,尤其是每個函數的使用。gogogo
#0x01 加載socket庫
從現在開始,程序要分為windows版和linux版,linux版就是加載一堆頭文件,windows呢就是加載dll后聲明一些函數,我們一點點開始,先看windows,想直接看linux的可以跳過windows
windows:
1 #include <stdio.h> 2 #include <winsock2.h>//socket頭文件 3 #pragma comment (lib,"ws2_32.lib")//加載socket 4 5 int main() 6 { 7 WSADATA wsaData;//生成句柄 8 WSAStartup(MAKEWORD(2, 2), &wsaData);//初始化 9 10 /*codding*/ 11 12 WSACleanup();//結束調用 13 return 0; 14 }
這就是windows的大體結構,ws2_32.lib用的是編譯時加載,具體的加載方式和顯示加載還是隱式加載大家可以自行查閱,這里不做深究,下面對函數進行分析:
首先看一下WSADATA結構體:
typedef struct WSAData { WORD wVersion; WORD wHighVersion; char szDescription[WSADESCRIPTION_LEN+1]; char szSystemStatus[WSASYS_STATUS_LEN+1]; unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR *lpVendorInfo; } WSADATA, *LPWSADATA;
具體每個變量是做什么的就要大廢周章了,大家可以點擊這里查看微軟官方的解釋,大概意思就是存放期望調用方使用的Windows套接字規范的版本
再看一下WSAStartup()函數,原型:
int WSAStartup( _In_ WORD wVersionRequested, _Out_ LPWSADATA lpWSAData );
第二個參數lpWSADATA就是WSADATA結構體,第一個WORD參數,用的MAKEWORD()函數,原型如下:
WORD MAKEWORD(
BYTE bLow,
BYTE bHigh
);
這個函數主要就是返回一個WORD參數。
接下來看看WSAClearup()函數:
int WSACleanup(void);
官方解釋是:The WSACleanup function terminates use of the Winsock 2 DLL(Ws2_32.dll).意思就是不再使用Ws2_32.dll庫了。
到此,socket庫的調用就說完了,具體還有很多很多沒有講,讀者可以自查資料深入研究,最后一節我會給使用到的鏈接,目前就會使用就行。
Linux:
linux下比較易於理解,主要就是包含一堆頭文件就行,demo如下:
1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 #include <arpa/inet.h> 6 #include <sys/socket.h> 7 #include <netinet/in.h> 8 int main() 9 { 10 /*codding*/ 11 }
前三個是C++基礎文件,不做贅述,第四個unisted.h是linux下特有的頭文件,主要包含了read,write等函數,后三個主要是socket需要的庫文件。
#0x02 socket函數
前面講了一大推分量很小的東西,現在才開始真正的socket編程,這里要明白一個概念,什么是句柄,這個概念在本案例中可以初步認為就是一個int(顯然這是錯誤的,目前先這么理解,因為在linux中並沒有句柄)。首先需要調用socket函數,分為windows和linux版本
windows:
SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
Linux:
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
其中SOCKET指的就是句柄,而在linux中用int代替。
現在已經創建一個套接字,供后面的程序使用,記住一個連接用一個套接字,什么意思,就是假如你的機器A和另一個機器B需要TCP連接,這時需要創建一個套接字,然后你的機器A和機器B需要另一個UDP連接,這時候你就需要再創建一個套接字。
win和linux用法是一樣的,我們來看一下他的原型:
SOCKET WSAAPI socket( _In_ int af, _In_ int type, _In_ int protocol );
下面看一個每個值得取值,這里僅僅列出常見取值,其他取值有興趣可以參考這個
af:協議族
- AF_INET(PF_INET):使用IPv4
- AF_INET6(PF_INET6):使用IPv6
type:套接字的類型
- SOCK_STREAM:面向連接的數據傳輸方式,也就是TCP連接
- SOCK_DGRAM:無連接的數據傳輸方式,也就是UDP連接
protool:傳輸協議(要和協議和套接字配套,本例指的是TCP或UDP)
- IPPROTO_TCP:TCP協議
- IPPROTO_UDP:UDP協議
好了,現在已經創建了套接字, 接下來就要給套接字綁定ip和端口了,查看bind()函數:
#0x03 bind()函數
值得一提的是,bind函數一般用在服務器上,也就是服務端,稍后會講解客戶端需要用什么函數,先看一下bind使用:
windows:
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));//Windows
Linux:
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
以上都是實例,參數可能不是很准確,所以看一下函數原型不就知道該怎么寫了嘛
int bind( _In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen );
其中,第一個參數s指的就是一開始創建的套接字,不在贅述,這里主要講一下后面兩個參數:
第二個參數是要傳入一個sockaddr型的結構體,而這個結構體長啥樣呢,我們來看一下
struct sockaddr{ sa_family_t sin_family; //地址族(Address Family),也就是地址類型 char sa_data[14]; //IP地址和端口號 };
第一個參數還好說,協議族,和創建socket的第一個參數一樣,俺么第二個參數呢,14位的地址和端口號應該怎么填?
這時候,我們就要借助另外一個結構體sockaddr_in(至於為什么要借助另外一個結構體,這里涉及到歷史遺留問題,感興趣可以自行查閱)
struct sockaddr_in{ sa_family_t sin_family; //地址族(Address Family),也就是地址類型 uint16_t sin_port; //16位的端口號 struct in_addr sin_addr; //32位IP地址 char sin_zero[8]; //不使用,一般用0填充 };
這里把每個變量位數已經標好了,可以看到位數和上一個結構體一樣,所以這里我們用sockaddr_in結構體定義好后替換成sockaddr結構體,都坐下,正常操作是:
sockaddr_in sockAddr; memset(&sockAddr, 0, sizeof(sockAddr));//各位清零 sockAddr.sin_family = PF_INET;//協議族 sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip地址 sockAddr.sin_port = htons(1996);//端口 bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));//Windows
首先創建一個sockaddr結構體,將各位清零(防止亂碼),然后設置協議族,ip地址,端口等,最后將這些信息綁定到最開始創建的句柄上,這樣一個套接字就完成了綁定
這里介紹一下inet_addr函數,這個函數是將一個ip字符串轉換成unsigned long,同樣也是歷史因素,想具體了解可以查看這里,不過zaiwindows下vs建議換成inetPton(),不過我就是任性啊,就不換,嘻嘻,在linux下這個函數可不可以替換我沒有驗證,如果有人驗證了可以在這篇文章下面留言
htons函數返回一個網絡字節數,看一下官方解釋,還有htonf()htonl()htonll()等函數,可以好好研究一下
#0x04 connent()函數
connent函數和bind函數用法一樣,只不過connent是客戶端的程序,我給列一下函數原型,用法可以在下篇看到
int connect( _In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen );
函數原型,參數和bind一樣,所以用法也一樣,不多說。
#0x05 listen()函數
listen函數需要在connect之前,在bind函數之后的服務器端的函數,看名字就知道是用來監聽的函數,用法:
Windows/Linux:
listen(servSock, 20);
原型是:
int listen( _In_ SOCKET s, _In_ int backlog );
比較容易理解,第一個參數就是bind后的套接字,而第二個參數指的是等待連接隊列的最大長度,啥意思?
就是說socket有2個緩沖區,一個是發送緩沖區,一個是接受緩沖區,他們有固定的大小,超過了這個大小,數據不就接收不到了嘛,這對文件傳輸可以一個麻煩,所以這個等待連接隊列指的是可以等待多少個緩沖區大小的數據,這里了解到這里就可以了,具體應用可以查看后面的文件傳輸內容在深入研究
#0x06 參考連接
【1】http://c.biancheng.net/cpp/html/3032.html
【2】http://blog.csdn.net/y396397735/article/details/50655363
【3】http://blog.csdn.net/tennysonsky/article/details/45621341
【4】http://msdn.microsoft.com
