Windows Socket编程总结
听会、看会、甚至把代码引用过来,不一定是真正的会了。
前几天去一家公司面试,有上机操作的题目,要求是这样的:开两个线程,一个是服务器线程一个是客户端线程,要求客户端给服务器发送一个“你好,服务器!”,服务器收到后给客户端回复“你好,客户端!”,然后客户端和服务器同时关闭。
由于最近一直在做一个屏幕监控的软件,socket很常用,但是大部分socket通信代码都是直接“引用过来的”,直接配置一下IP和端口,甚至,后来还把IP 和端口放到了.ini文件中。所以自己写出来的socket代码千疮百孔,惨不忍睹。。。回来后痛定思痛,认真的总结了以下socket通信的步骤:(以TCP为例)
TCP的步骤很规范:(总体思路是这样,可能和一些教科书有出入)
服务器端:
1 创建套接字 socket |
2 定义地址信息(IP /PORT) SOCKADDR_IN |
3 绑定 bind |
4 监听 listen |
5 接收连接 accept |
6收发数据send/recv |
客户端:
1 创建套接字 |
2 定义地址(这里是服务器地址) SOCKADDR_IN |
3 连接 connect |
4 收发数据 send/recv |
MSDN中原话如下:
The WSAStartupfunction initiates use of Ws2_32.dll by a process。
意思就是你如果想用socket那套函数,得先做一些初始化:
1 引入头文件: #include <Winsock2.h>
2 在工程中加入socket的动态库文件:在Project->Settings->Link在 【Object/library modules:】一项中加入Ws2_32.lib (至于动态库为什么是 .lib结尾的,这里不做赘述,可以看看动态库那部分知识),然后点击OK。
3 WSAStartup代码参考 MSDN 中 WSAStartup下面的实例代码,直接粘过来就OK。二、服务器端编程
做完了初始化,现在到服务器端的编写了
1 创建套接字
SOCKET sockServer;
sockServer=socket(AF_INET,SOCK_STREAM,0);
//第一个参数定义了Address family specification,windows下只有AF_INET
//第二个参数是选择UDP(SOCK_DGRAM) 、 TCP(SOCK_STREAM) 方式
//第三个设置成0就可以了
2 定义地址信息
// sockaddr_in的结构如下
struct sockaddr_in{
short sin_family; //地址族,对于IP地址,必须是AF_INET
unsigned short sin_port;//端口
IN_ADDR sin_addr;//ip地址
char sin_zero[8];//只是一个填充数
};
//实际的代码:
SOCKADDR_IN addr={0};
addr.sin_family=AF_INET;
addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//把IP地址转换成网络字节序,。
addr.sin_port=htons(8899);//端口 需大于 1024
此处注意:
a) 这里用SOCKADDR_IN 代表 SOCKADDR是因为 SOCKADDR是为所有的地址家族准备的,这个结构可能(通常会)随着所使用的网络协议不同而不同。在TCP/IP协议中,用SOCKADDR_IN结构来替换sockaddr,以方便填写地址信息。
b)INADDR_ANY允许套接字向任何分配给本地机器的IP地址发送或者接收数据。例如多个网卡的机器,每个网卡都有自己的IP,用INADDR_ANY允许一个独立应用接收发自多个接口的回应
c) htonl是把一个unsigned long的数转换为一个网络字节序
3 绑定
bind(sockServer,(SOCKADDR*)&addr,sizeof(SOCKADDR))
//把 sockServer绑定到addr的地址,由于网络协议的不确定性,最后一个参数是addr地址的大小
4 监听
listen(sockServer,5)
//监听连接请求,第一个是连接套接字,第二个是等待连接队列的最大长度,注意此处不是在一个端口最大连接数
5 接收连接
SOCKADDR_IN addrClient={0};
int len=sizeof(SOCKADDR);
char* buffer=new char[50];
SOCKET sockClient=accept(sockServer,(SOCKADDR*)&addrClient,&len);
// 第一个参数是服务器套接字,第二个是接收客户端的地址,第三个是地址的长度
//accept的返回值是接收的客户端的socket
//本机测试正常情况下,如果之前绑定和监听没有问题的话,accept是阻塞的
6 收发数据
//前边几部都没有错误的话,服务器就可以收发数据了,此处是客户端先发送数据,服务器再回复:
// recv对应的是已经建立连接的套接字
if (SOCKET_ERROR==recv(sockClient,buffer,50,0))
{
AfxMessageBox("接收数据失败");
return FALSE;
}
CString str;
str.Format("%s",buffer);
AfxMessageBox(str);
memset(buffer,0,50);
strcpy(buffer,"我是服务器,客户端,我收到了!");
//此处发送服务器信息到客户端,收发都是以已经建立连接对应的套接字
send(sockClient,buffer,strlen(buffer)+1,0);
//释放资源
delete[] buffer;
closesocket(sockServer);//关闭套接字
WSACleanup();
三、客户端编程
客户端代码跟服务器代码很类似
首先初始化资源。。。
其次是socket编程:
SOCKET sockClient;
sockClient =socket(AF_INET,SOCK_STREAM,0);
2 定义IP和端口资源
//1 定义资源
SOCKADDR_IN addr={0};//
addr.sin_family=AF_INET;//Address family; must be AF_INET.
addr.sin_addr.S_un.S_addr=inet_addr(“127.0.0.1”);
addr.sin_port=htons(8899);
3 连接
int ret=connect(sockClient,(SOCKADDR*)&addr,sizeof(SOCKADDR));
if (ret==SOCKET_ERROR)
{
return FALSE;
}
//针对对服务器端的IP和端口进行连接
4 收发数据
char* buffer=new char[50];
strcpy(buffer,"我是客户端,服务器你收到了吗");
send(sockClient,buffer,strlen(buffer)+1,0);
recv(sockClient,buffer,50,0);
AfxMessageBox(CString(buffer));
//由于客户地址不需要进行绑定,所以直接用已建立连接的SOCKET进行数据的收发
//最后释放资源。。。
delete[] buffer;
closesocket(sockClient);
WSACleanup();
多线程部分略。。。详见代码
四、UDP
UDP服务器端通信
1 定义套接字 socket |
2 初始化IP/端口资源 SOCKADDR_IN |
3 绑定 bind |
4 收发数据 recvfrom/ sendto |
UDP客户端通信
1 定义套接字 socket |
2 指定服务器IP/端口资源 SOCKADDR_IN |
3 收发数据 recvfrom/ sendto |
五、总结
1 一定按照指定网络字节序初始化IP和PORT
几个有用的函数(详细介绍见MSDN)
inet_addr-把点分十进制的IP地址转化为in_addr结构体的地址;
inet_ntoa-把in_addr格式的IP地址转换成一个点分十进制IP地址;
htons -把一个u_short类型的主机序列数转化成一个网络字节序的u_short;
htonl -把一个u_long类型的主机序列数转换成一个网络字节序的u_long
2 注意服务器端,不要用绑定的socket收发数据,而是用建立连接的socket收发数据
3 UDP连接方式中,sendto的第五个参数const struct sockaddr *to,一定搞清楚地址。
socket编程看似简单,主要思想也就那么几步,但是很容易出问题。我面试时候的问题在于:没有按照指定的网络字节序初始化IP和PORT,多么痛的领悟!