-----初更----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