1 Winsocket编程原理
1.1 套接字(sockets)
套接字是通信的基石,是支持TCP/IP的网络通信的基本操作单元。可以将套接字看作不同主机间的进程进行双向通信的端点,它构成了在单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中套接字交换数据。
套接字有两种不同的类型:流套接字和数据报套接字。
1.2 C/S模型(客户机/服务器模型)
客户机/服务器模型工作时要求有一套为客户机和服务器所共识的惯例来保证服务能够被提供(或被接受),这一套惯例包含了一套协议,它必须在通信的两头都被实现。根据不同的实现情况,协议可能是对称的或是非对称的。在对称的协议中,每一方都有可能扮演主从角色;在非对称协议中,一方被不可改变地认为是主机,而另一方则是从机。一个协议的例子是Internet中用于终端仿真的Telnet,而非对称协议的例子是Internet中的HTTP。无论具体的协议是对称的或是非对称的,当服务被提供时必然存在客户进程和服务进程。
一个服务程序通常在一个众所周知的地址监听客户对服务的请求,也就是说,服务进程一直处于休眠状态,直到一个祝词对这个服务的地址提出了连接请求。如下图:
1.3 Winsock的启动和终止
由于Winsock的服务是以动态链接库WinsockDLL形式实现的,所以必须先调用WSAStartup函数对Winsock DLL进行初始化,协商Winsock的版本支持,并分配必要的资源。如果在调用Winsock之前没有加载Winsock库,则会返回SOCKET_ERROR错误,错误信息是WSANOTINITIALISED。WSAStartup函数的原型如下:
Int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
其中,参数wVersionRequested用于指定准备加载的Winsock库的版本;通常的做法是高位字节指定所需要的Winsock库的副版本,而低位字节则是主版本,然后,用宏MAKEWORD(X,Y)(X是高位,Y是低位字节)获得wVersionRequested的正确值。lpWSAData参数是指向LPWSADATA结构的指针,该结构包含了加载的库版本有关的信息,它的格式如下:
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;
其中,wVersion字段为打算使用的Winsock版本。字段wHighVersion返回现有Winsock库的最高版本。szDescription和szSystemStatus这两个字段由特定的Winsock实施方案设定,事实上没有用。字段iMaxSockets和iMaxUdpDg分别为可同时打开的套接字数和数据报的最大长度,一般不要使用它们,若想知道数据报的最大长度则应该通过WSAEnumProtocols函数来查询协议信息。最后一个字段lpVendorInfo是为Winsock实施方案有关的指定厂商信息预留的,任何一个Win32平台上都没有使用这个字段。
另外在应用程序关闭套接字后,还就调用WSACleanup函数终止对Winsock DLL的使用并释放资源,以备下一次使用。WSACleanup函数原型如下:
int WSACleanup(void);
该函数不带任何参数,若调用成功则返回0,否则返回错误。
1.4 Winsock编程步骤
下面介绍采用C/S方式的流套接字编程模型,它的时序图如下图:
1.4.1 加载/释放Winsock库
WSADATA wsaData;
if(WSAStartup(MAKEWORD(1,1), &wsaData) != 0 ) {
return1;
}
if(LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup(); //释放winsock库
return2;
}
1.4.2 服务器/客户进程创建套接字SOCKET
服务器进程总是先于客户进程启动,服务进程首先调用socket函数创建一个流套接字。
Socket函数的原型如下:
SOCKET socket(int af,inttype,int protocol);
其中,参数af用于指定网络地址类型,一般取AF_INET,表示该套接字在Internet域中进行通信。参数type用于指定套接字类型,若取SOCK_STREAM表示创建的套接字是流套接字,而取SOCK_DGRAM创建的是数据报套接字。参数protocol用于指定网络协议,一般取0,表示默认为TCP/IP协议。若套接字创建成功则该函数返回所创建套接字句柄SOCKET,否则产生INVALID_SOCKET错误。
服务端:构造监听SOCKET,流式SOCKET.
SOCKET sockService = socket(AF_INET,SOCK_STREAM, 0)
客户端:构造通讯SOCKET,流式SOCKET.
SOCKET socketClient =socket(AF_INET, SOCK_STREAM, 0)
1.4.3 绑定SOCKET
将本地址绑定到所创建的套接字上以使在网络上标识该套接字。这个过程通过调用bind函数来完成,该函数原型如下:
int bind(SOCKET s,const struct sockaddr *name,int namelen);
其中,第一个参数s标识一未捆绑套接字句柄,它用来等待客户机的连接。第二个参数name是赋予套接字的地址,它由structsockaddr结构表示,由于该结构随选择协议的不同而变化,因此一般情况下另一个与该地址结构大小相同的socketaddr_in结构更为常用,socketaddr_in结构用来标识TCP/IP协议下的地址,socketaddr_in结构的格式如下:
struct sockaddr_in
{
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其中,sin_family字段必须设为AF_INET,表示该socket处于Internet域。sin_port字段用于指定服务端口。sin_addr字段用于把一个IP地址保存为一个4字节的数,它是无符号长整数类型;最后一个字段sin_zero,只充当填充项的职责,以使sockaddr_in结构和sockaddr结构的长度一样。
一旦出错,bind函数就会返回SOCKET_ERROR。
故在绑定之前我们要先设置好服务器端和客户端的地址和端口信息。
服务器端:
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr= INADDR_ANY;
addrSrv.sin_family =AF_INET;
addrSrv.sin_port=htons(5566);
客户端:
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr= inet_addr("127.0.0.1"); //远程主机的IP
addrSrv.sin_family =AF_INET;
addrSrv.sin_port =htons(5566);
需要说明一下的是,127.0.0.1是本机IP,也就是说我在做的时候主机即当服务器又当客户。
接下来服务器端就可以绑定了:
bind(sockService,(SOCKADDR*)&addrSrv,sizeof(addrSrv));
1.4.4 监听listen
Bind函数的作用只是将一个套接字和一个指定的地址关联在一起,让一个套接字等候进入连接的API函数是listen,其原型为:
int listen(SOCKET s,int backlog);
其中,参数s 标识一个已绑定未连接套接字的描述字。Backlog参数用于指定正在等待连接的最大队列长度,这个参数非常重要,因为完全可能同时出现几个服务器连接请求。
如果正确监听,listen函数返回0,若失败则返回SOCKET_ERROR错误。
1.4.5 服务端/客户端连接:
进入监听状态后,通过调用accept函数使套接字作好接受客户连接的准备,accept函数原型为:
SOCKET accept(SOCKET s,struct sockaddr *addr,int *addrlen);
其中参数s是处理监听模式的套接字描述字。第二个参数应该是一个有效的SOCKADDR_IN结构的地址,而addrlen是SOCKADDR_IN结构的长度。这样,服务器便可以为等待连接队列中的第一个连接请求提供服务了。如果无连接请求,服务进程被阻塞。
当服务器端接受客户器就绪之后,客户向服务进程发出连接请求。通过connect函数可以建立一个端的连接。Connect函数原型为:
int connect(SOCKET s,const struct sockaddr FAR *name,int namelen);
其中,s标识一个未连接的数据报或流类套接字描述字。Name是针对TCP的套接字地址结构,它标识服务进程IP地址信息。
1.4.6 收/发数据
当服务端和客户端成功建立好连接之后,就可以进行收发通信了。分别用send,recv函数进行数据收发。
send函数原型如下:
int send(SOCKET s,const char *buf,int len,int flags);
其中,SOCEKT参数是已经建立连接的套接字描述字,表示发送数据操作将在这个套接字上进行。第二个参数buf是字符缓冲区,包含即将发送的数据。第三个参数len用于指定即将发送的缓冲区内的字符数。最后一个参数flags可取0、MSG_DONTROUTE或MSG_OOB或这些标志位的按位“或”运算,MSG_DONTROUTE标志要传送层不要将它发出的包路由出去,MSG_OOB标志数据应该被带外发送。
recv函数原型为:
int recv(SOCKET s,char *buf,int len,int flags);
参数含义和send函数差不多,recv函数返回发送字节数。
1.4.7 关闭SOCKET
一旦任务完成,就必须关掉连接以释放套接字战胜的所有资源。通常调用closesocket函即可达到目的,但closesocket可能会导致数据的丢失,因此应该在调用该函数之前,用shutdown函数从容地中断连接,即发送端通知接收端“不再发送数据”或通知发送端“不再接收数据”。
shutdown函数原型为:
int shutdown(SOCKET s,int how);
其中how的取值有:SD_RECEIVE,SD_SEND或SD_BOTH分别表示:不允许再调用接收函数、不允许再调用发送函数、取消连接两端的收发操作。
closesocket()函数原型为:
int closesocket(SOCKET s);
1.5 一个简单的C/S通信
1.5.1 Client.cpp
#include<iostream>
#include<Winsock2.h>
#include<string>
#include"winsock.h"
#define MAX_BUF 1024
#pragma comment(lib,"wsock32")
using namespace std;
intmain()
{
WSADATA wsaData;
if(WSAStartup(MAKEWORD(1, 1 ), &wsaData ) != 0)
{
return 1;
}
if(LOBYTE(wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1)
{
WSACleanup();
return 2;
}
//构造SOCKET
SOCKET sockClient = socket(AF_INET,SOCK_STREAM,0);
//配置监听地址和端口
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); //远程主机的IP
addrSrv.sin_family= AF_INET;
addrSrv.sin_port= htons(5566);
//连接
if(connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(addrSrv)) == SOCKET_ERROR)
{//连接失败
cout<<"ConnectError: "<<WSAGetLastError()<<endl;
return 3;
}
else
cout<<"和服务器连接成功..."<<endl;
char sendBuf[MAX_BUF];
char recvBuf[MAX_BUF];
while(true)
{
//向服务器发送数据
gets(sendBuf);
sendBuf[MAX_BUF]= '\0';
send(sockClient,sendBuf,strlen(sendBuf),0);
//从服务器端接收数据
int nRecv = recv(sockClient,recvBuf,MAX_BUF,0);
if(nRecv> 0)
{
recvBuf[nRecv]= '\0';
cout<<recvBuf<<endl;
}
}
//关闭
closesocket(sockClient);
WSACleanup();
return0;
}
1.5.2 Service.cpp
#include<iostream>
#include<Winsock2.h>
#include"winsock.h"
#define MAX_BUF 1024
#pragma comment(lib,"wsock32")
using namespace std;
intmain()
{
//加载winsock库
WSADATA wsaData;
if(WSAStartup(MAKEWORD(1,1), &wsaData) != 0 ) {
return 1;
}
if(LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup(); //释放winsock库
return 2;
}
//创建socket
SOCKET sockService=socket(AF_INET,SOCK_STREAM,0);
//配置监听地址和端口
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr= INADDR_ANY;
addrSrv.sin_family= AF_INET;
addrSrv.sin_port=htons(5566);
//绑定socket
if(bind(sockService,(SOCKADDR*)&addrSrv,sizeof(addrSrv))== SOCKET_ERROR)
{
cout<<"Failedbind()"<<endl;
return 3;
}
//监听
if(listen(sockService,2)== SOCKET_ERROR)
{
cout<<"Failedlisten()"<<endl;
return 4;
}
//C/S连接
SOCKADDR_IN addrClient;
int len = sizeof(addrClient);
SOCKET sockConn = accept(sockService,(SOCKADDR *)&addrClient,&len);
if(sockConn== INVALID_SOCKET)
{
cout<<"connectfailed"<<endl;
return 5;
}
else
cout<<"和客户端已建立连接..."<<endl;
char recvBuf[MAX_BUF];
char sendBuf[MAX_BUF] = "Recieved";
while(true)
{
//从客户端接收消息
int nRecv = recv(sockConn,recvBuf,MAX_BUF,0);
if(nRecv> 0)
{
recvBuf[nRecv]= '\0';
cout<<recvBuf<<endl;
}
//向客户端发消息
send(sockConn,sendBuf,strlen(sendBuf),0);
}
//关闭socket
closesocket(sockConn);
closesocket(sockService);
return0;
}
运行结果如下图: