学习Winsock API编程
Windpows Sockets 是广泛应用的、开放的、支持多种协议的网络编程接口,主要由winsock.h头文件和动态链接库winsock.dll组成。
一、套接字
套接字(Sockes)是通信的基础,是支持TCP/IP协议的网络通信的基本操作单元。可以将套接字看作是不同主机之间的进程进行双向通信的端点。根据通信网络的特性,套接字可以分为以下两类。
1、流套接字
流套接字提供没有边界的数据流(即字节流),能够确保数据流以正确的顺序无重复地被送达,使用于处理大量数据。流套接字是面向连接的。
2、数据报套接字
数据报套接字支持双向数据流,此数据流不能保证按顺序和不重复送达,也不能保证数据传输的可靠性。数据报套接字是无连接的。
Winsock对有可能阻塞的函数提供了两种处理方式:阻塞方式和非阻塞方式。在阻塞方式下,收发数据的函数在被调用后一直等到传送完毕或出错才能返回,期间不能进行任何操作。在非阻塞方式下,函数被调用后立即返回,当网络传送完后,由Winsock给应用程序发送一个消息,通知操作完成。在编程时,应尽量使用非阻塞模式。
二、Winsock的启动和终止
由于Winsock服务是以动态链接库的形式实现的,所以在使用前必须调用WSAStartup函数对其进行初始化,协商Winsock的版本支持,并分配必要的资源。WSAStartup函数声明如下:
int WSAStartup(WORD wVersionRequested,LPWSADATA IpWSAData);
参数说明:
◇wVersionRequested:指定加载的Winsock版本,通常高位字节指定Winsock 的副版本,低位字节指定Winsock的主版本,然后用MAKEWORD(X,Y)宏获取该值。
◇IpWSAData:WSADATA数据结构指针,其中WSADATA结构的定义如下:Typedef struct WSAData{
WORD wVersion; //期望使用的Winsock版本
WORD wHighVersion; //返回现有Winsock最高版本
char szDescription[WSADESCRIPTION_LEN+1];//套接字实现描述、
char szSystemStatus[WSASYS_STATUS_LEN+1];//状态或配置信息
unsigned short iMaxSockets; //最大套接字数 unsigned short iMaxUdpDg; //最大数据报长度 char FAR * IpVendorInfo; //保留 }WSADATA,FAR *LPWSADATA; 在应用程序关闭套接字连接后,还需要调用WSACleanup 函数终止对Winsock 库的使用,并释放资源,函数声明如下:
int WSACleanup(void);
三、 Winsock 编程模型
不论是流套接字还是数据报套接字编程,一般都采用客户端/服务器模式,其运行原理基本类似。数据报套接字的编程模型如图一所示。流套接字的编程模型如图二所示。
图一 数据报套接字编程模型
图二 流套接字编程模型
流套接字的服务进程和客户端进程在通信前必须创建各自的套接字并建立连接,然后才能实现数据传输。具体编程步骤如下:
(1) 服务器进程创建套接字。
(2) 将本地地址绑定到套接字上以标识该套接字。 (3) 将套接字置入监听模式并准备接收连接请求。 (4) 客户端进程调用socket 函数创建客户端套接字。 (5) 客户端进程向服务进程发出连接请求。 (6) 数据传输。 (7) 关闭套接字。
服务器 服务器
服务器进程总是先于客户进程启动,调用socket创建一个流套接字,该函数声明如下:
SOCKET socket(int af,int type,int protocol);
参数说明:
◇af:指定网络地址族,一般为AF_INET。
◇type:指定套接字类型,可选的取值如下:
△SOCK_STREAM 流套接字。
△SOCK_DGRAM 数据报套接字。
◇protocol:指定网络协议,一般为0,表示默认的TCP/IP协议。
成功创建了Socket之后,就应该选定通信的对象。调用bind()函数可以将本地地址绑定到套接字上,该函数声明如下:
int bind(SOCKET s,const struct sockaddt FAR* name ,int namelen); 参数说明:
◇s:指定一个未绑定的套接字句柄,用于等待客户进程的连接。
◇name:指向sockaddr结构对象的指针。
◇namelen:指定sockaddr结构的长度。
其中sockddr结构随选择的协议的不同而变化,因此常用的是sockaddr_in 结构,用来标识TCP/IP协议下的地址,该结构定义如下:
struct sockaddr_in{
short sin_family; //指定地址族,一般为AF_INET
u_short sin_port; //指定端口号
struct in_addr sin_addr; //指定IP地址
char sin_zero[8]; //填充位
};
其中IP地址结构in_addr的定义如下:
struct in_addr{
union{
struct{u_char s_b1,s_b2,s_b3,s_b4;}S_un_b;
struct{u_short s_w1,s_w2;}S_un_w;
u_long S_addr;
}S_un;
};
绑定成功后,调用listen函数用于设置套接字的等待连接状态,该函数声明如下:
int listen(SOCKET s,int backlog);
参数说明:
◇s:指定一个已绑定未连接的套接字句柄。
◇backlog:指定正在等待连接的队列的最大长度,可取1~5。
进入监听状态后,通过调用accept函数使套接字做好接受客户连接的准备,该函数声明如下:
SOCKET accept(SOCKET s,struct sockaddr FAR* addr,int FAR* addrlen);
参数说明:
◇s:指定处于监听状态的套接字句柄。
◇addr:指定一个有效的SOCKADDR_IN结构地址。
◇addrlen:指定 SOCKADDR_IN结构的长度。
accept函数返回后,addr变量中会包含请求连接的客户IP地址,并返回一个新的套接字句柄,对应于已经接受的那个客户端连接。而原来的监听套接字仍处于监听状态。
客户进程调用connect函数可以主动提出连接请求,该函数声明如下:
int connect(SOCKET s,const struct sockaddr FAR* name,int namelen); 参数说明:
◇s:指定一个未连接的套接字句柄。
◇name:指定服务进程的IP地址信息,针对TCP协议。
◇namelen:指定name参数的长度。
当服务器进程接受连接请求后,将生成一个新的套接字,并向各客户进程返回接受信号。一旦客户进程收到来自服务器的接受信号,表示建立连接,即可进行数据传输了。调用send函数用于发送数据,调用recv函数用于接受数据,函数声明如下:
int send(SOCKET s,const char FAR* buf,int len,int flags);
int recv(SOCKET s,const char FAR* buf,int len,int flags);
参数说明
◇s:指定已建立连接的套接字。
◇buf:发送或接受数据缓冲区。
◇len:指定数据缓冲区的长度。
◇flags:标志,一般为0。
通信结束,必须关掉连接以释放套接字占用的资源。调用closesocket函数用于关闭套接字,该函数声明如下:
Int closesocket( SOCKET s);
为了保证套接字正常关闭,一般在调用closesocket之前先调用shutdown函数中断连接。该函数声明如下:
int shutdown(SOCKET s,int how);
参数说明:
◇s:指定要中断的套接字句柄。
◇how:指定将禁止的操作,可选的取值如下:
△SD_RECEIVE 禁止调用接受函数。
△SD_SEND 禁止调用发送函数。
△SD_BOTH 取消收发操作。
四、Winsock I/O模型
Winsock套接字在两种模式下执行I/O操作,即阻塞和非阻塞。默认情况下,套接字为阻塞模式。
下面的代码演示了创建一个套接字,并将其设置为非阻塞模式的过程:
SOCKET s; //套接字句柄
unsigned long cmd; //指令参数
int nStatus; //返回值
s = socket(AF_INET,SOCK_STREAM,0); //创建流套接字
nStatus = ioctlsocket(s,FIOBIO,&cmd); //设置为非阻塞模式
将一个套接字设置为非阻塞模式之后,Winsock API调用会立即返回。Winsock提供了几种不同的套接字I/O模型,如选择(Select)、异步选择(WSAAsyncSelect)、事件选择(WSAEventSelect)和重叠(Overlapped)等。
1.WSAAsyncSelect模型
利用异步选择模型,应用程序可以在一个套接字上接收以Windows消息为基础的网络事件通知。该模型的实现方法是通过调用WSAAsyncSelect函数自动将套接字设置为非阻塞模式,并注册一个或多个网络事件,提供一个消息通知的窗口句柄。当注册的网络事件发生时,对应的窗口将接收到一个基于消息的通知。WSAAsyncSelect函数声明如下:
int WSAAsyncSelect(SOCKET s,HWND hWnd,unsigned int wMsg,long IEwent); 参数说明:
◇s:指定需要事件通知的套接句柄。
◇hWnd:指定接收消息的窗口句柄。
◇wMsg:指定发送的消息。
◇IEvent:指定网络事件集合,可以是以下取值的和:
△FD_READ 想要接收读准备好的通知。
△FD_WRITE 想要接收写准备好的通知。
△FD_OOB 想要接收带外数据到达的通知。
△FD_ACCEPT 想要接收连接准备好的通知。
△FD_CONNECT 想要接收已经连接的通知。
△FD_CLOSE 想要接收套接字关闭的通知。
如果要取消所有的通知,则将IEvent参数设置为0即可。
2.WSAEventSelect模型
与异步选择模型相似,利用事件选择模型应用程序可以在一个或多个套接字上接收以事件为基础的网络事件通知,并且它支持的网络事件与异步选择模型一样。它与WSAAsyncSelect模型最主要的区别在于,网络事件会被发送到一个事件对象句柄,而不是一个窗口句柄。首先需要调用WSACreateEvent函数创建事件对象来接收网络事件,该函数声明如下:
WSAEVENT WSACreateEvent(void);
返回的事件对象具有两种工作状态:有信号和无信号。
接着调用WSAEventSelect函数将所创建的事件对象与套接字关联起来,并注
册网络事件,该函数声明如下:
Int WSAEventSelect(SOCKET s,WSAEVENT hEventObject,
Long INetworkEvents);
参数说明:
◇s:指定需要事件通知的套接字句柄。
◇hEventObject:指定事件对象句柄。
◇InetworkEvent:指定网络事件集合。
在完成一个I/O操作之后应用程序需要调用WSAResetEvent函数重置该事件对象,函数声明如下:
BOOL WSAResetEvent(WSAEVENT hEvent);
参数说明
◇hEvent:用于指定事件对象句柄。
一个套接字与一个事件对象句柄关联起来之后,应用程序就可以通过WSAWaitForMultipleEvents函数等待网络事件来触发事件句柄的工作状态,进行I/O操作,该函数声明如下:
DWORD WSAWaitForMultipleEvents(DWORD cEvents,
const WSAEVENT FAR *IphEvents,
BOOL fWaitAll,DWORD dwTimeOUT,BOOL fAlertable);
参数说明:
◇cEvent:指定事件对象句柄数组的元素的个数。、
◇IphEvents:指向一个事件对象句柄数组的指针。
◇fWaitAll:指定是否等待所有事件对象同时有信号。
◇dwTimeOUT:指定超时等待时间。
◇fAlertable:指定当系统将I/O例程放入队列时,函数是否返回。
五、应用实例:基于Winsock API调用的聊天室
1、服务器端应用程序设计
(1) 首先建立一个空的工作空间E1701。
(2) 在空间E1701里建立一个对话框工程Server作为服务端。对话框设计如图
三所示。
图三服务器端窗口设计
(3) 在类CServerDlg中给显示聊天内容和发送聊天内容的文本框添加变量
// Dialog Data
//{{AFX_DATA(CServerDlg)
enum { IDD = IDD_SERVER_DIALOG };
CEdit m_Show; // 聊天记录显示控件对象
CString m_strShow; // 聊天记录字符串
CString m_strMsg; // 聊天内容字符串
//}}AFX_DATA
(4) 在服务器端要保存客户端的socket连接,故引入链表支持,在ServerDlg.h 中添加代码:
#include
typedef CList
(5) 要调用Winsock API必须包含winsock.h头文件和动态链接库winsock.dll。在StdAfx.h文件中添加如下代码:
#include
#pragma comment(lib, "wsock32.lib")
(6) 自定义消息,并添加消息处理函数。在Server.h中添加:
#define WM_SERVERMSG (WM_USER+100)
// Generated message map functions
//{{AFX_MSG(CServerDlg)
afx_msg long OnServerMsg(WPARAM wParam, LPARAM lParam);
//}}AFX_MSG
(7) 初始化服务器窗口,创建服务器端socket。
BOOL CServerDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// TODO: Add extra initialization here
try
{
WSADATA wsaData; // WSADATA结构对象
WORD wVersionRequested=MAKEWORD(2, 0);// 指定Winsock版本为2.0 WSAStartup(wVersionRequested, &wsaData); // 启动Winsock
m_hSocket = socket(AF_INET, SOCK_STREAM, 0); // 创建流套接字
UINT len=WSAGetLastError();//获取错误代码
if(len!=0)
throw len;//抛出异常错误
m_saList.RemoveAll(); // 清空套接字列表
WSAAsyncSelect(m_hSocket,
this->m_hWnd, // 接收消息的窗口为对话框
WM_SERVERMSG, // 指定消息
FD_ACCEPT | FD_READ | FD_WRITE| FD_CLOSE);//指定事件m_uPort = 8080; // 设置端口号
// 设置套接字地址结构对象
m_addr.sin_family = AF_INET;
m_addr.sin_addr.S_un.S_addr = INADDR_ANY;
m_addr.sin_port = htons(m_uPort);
bind(m_hSocket,(LPSOCKADDR)&m_addr,sizeof(m_addr));//绑定套接字
listen(m_hSocket, 3); // 进入监听状态
len=WSAGetLastError();
if(len!=0)
throw len;
m_strShow = _T("服务器启动成功……");
}
catch(UINT &error)
{
switch(error)
{
case WSANOTINITIALISED:
MessageBox("创建套接字失败!");
m_strShow = _T("创建套接字失败!");
break;
case WSAEINVAL:
MessageBox("监听端口已被占用!");
m_strShow = _T("服务器启动失败");
break;
default:
MessageBox("监听端口已被占用!");
m_strShow = _T("服务器启动失败");
}
}
UpdateData(FALSE); // 更新显示
return TRUE;
}
(8) 修改OnDestroy()函数
void CServerDlg::OnDestroy()
{
CDialog::OnDestroy();
WSAAsyncSelect(m_hSocket, this->m_hWnd, 0, 0);// 取消异步选择模式WSACleanup(); // 清理Winsock
}
(9) 编写发送按钮函数
void CServerDlg::OnButton1()
{
UpdateData(TRUE);
m_strShow += _T("\r\n"); // 回车、换行
m_strShow += m_strMsg; // 添加聊天内容
SOCKET s;
for (int i=0; i { // 向每个客户端发送聊天内容 s = m_saList.GetAt(m_saList.FindIndex(i)); int t=send(s, m_strMsg.GetBuffer(0), m_strMsg.GetLength(), 0); if(t <0) { closesocket(s); m_saList.RemoveAt(m_saList.FindIndex(i)); } } m_strMsg.Empty(); UpdateData(FALSE); // 更新显示 m_Show.LineScroll(m_Show.GetLineCount()); // 跟踪滚动条的位置 } (10) 编写消息处理函数 long CServerDlg::OnServerMsg(WPARAM wParam, LPARAM lParam) { SOCKET socket, s; int i,j; char buf[1024],name[6],buf1[1024]; int len; switch(lParam) { case FD_ACCEPT: socket = accept(m_hSocket, NULL, NULL); for(i=0; i { s = m_saList.GetAt(m_saList.FindIndex(i)); UintToChar(socket,name); buf[0] = NULL; strcat(buf, "游客 "); strcat(buf,name); strcat(buf, " 进入聊天室"); len=send(s, buf, strlen(buf), 0); if(len <0) { closesocket(s); m_saList.RemoveAt(m_saList.FindIndex(i)); } } m_strShow += "\r\n"; UintToChar(socket,name); m_strShow+="游客 "; m_strShow+=name; m_strShow += " 进入聊天室"; UpdateData(FALSE); m_saList.AddHead(socket); return 0; case FD_READ: length = m_saList.GetCount(); for(i=0; i { s = m_saList.GetAt(m_saList.FindIndex(i)); if(s == wParam) { len = recv(s, buf, 1024, 0); UintToChar(wParam,name); buf[len] = NULL; buf1[0] = '\0'; strcat(buf1, "游客 "); strcat(buf1,name); strcat(buf1, " 说:"); strcat(buf1, buf); for(j=0; j { s = m_saList.GetAt(m_saList.FindIndex(j)); if(s!=wParam) { len=send(s, buf1, strlen(buf1),0); if(len <0) { closesocket(s); m_saList.RemoveAt(m_saList.FindIndex(j)); } } } len=WSAGetLastError(); len=WSAENOTCONN; m_strShow += "\r\n"; m_strShow += buf1; UpdateData(FALSE); return 0; } } return 0; case FD_WRITE: return 0; case FD_CLOSE: UintToChar(wParam,name); m_strShow += "\r\n"; m_strShow +="游客 "; m_strShow +=name; m_strShow +=" 断线"; UpdateData(FALSE); return 0; default: UintToChar(wParam,name); m_strShow += "\r\n"; m_strShow +="游客 "; m_strShow +=name; m_strShow +=" 离开"; UpdateData(FALSE); return 0; } } 2、客户端应用程序设计 (1) 在工作间E1701中创建一个对话框工程Client作为客户端。对话框设计如图四所示。 图四客户端窗口设计 (2)在类CClientDlg中给显示聊天内容、发送聊天内容、端口号和服务器IP的 文本框添加变量: // Dialog Data //{{AFX_DATA(CClientDlg) enum { IDD = IDD_CLIENT_DIALOG }; CEdit m_Show; CIPAddressCtrl m_ip;//IP地址 UINT m_uPort;//端口号 CString m_strShow; CString m_strMsg; //}}AFX_DATA (3) 要调用Winsock API必须包含winsock.h头文件和动态链接库winsock.dll。在StdAfx.h文件中添加如下代码: #include #pragma comment(lib, "wsock32.lib") (4) 自定义消息,并添加消息处理函数。在Client.h中添加: #define WM_CLIENTMSG (WM_USER+200) // Generated message map functions //{{AFX_MSG(CClientDlg) afx_msg long OnClientMsg(WPARAM wParam, LPARAM lParam); //}}AFX_MSG (5)初始化客户端窗口,创建socket。准备建立连接 BOOL CClientDlg::OnInitDialog() {//窗口初始化函数 CDialog::OnInitDialog(); // TODO: Add extra initialization here WSADATA wsaData; WORD wVersionRequested = MAKEWORD(2, 0); WSAStartup(wVersionRequested, &wsaData); m_hSocket=socket(AF_INET,SOCK_STREAM,0);//调用socket函数创建套接字WSAAsyncSelect(m_hSocket, this->m_hWnd, WM_CLIENTMSG, FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE);//利用异步选择模型,自动设置为非阻塞模式return TRUE; // return TRUE unless you set the focus to a control } (6)编写连接按钮函数 void CClientDlg::OnButton2() { UpdateData(true); BYTE f0,f1,f2,f3; m_ip.GetAddress(f0,f1,f2,f3); CString addr; addr.Format("%d.%d.%d.%d", f0, f1, f2, f3); m_addr.sin_family = AF_INET; m_addr.sin_addr.S_un.S_addr = inet_addr(addr.GetBuffer(0)); m_addr.sin_port = htons(m_uPort); connect(m_hSocket,(LPSOCKADDR)&m_addr,sizeof(m_addr));//连接服务器UINT tt=WSAGetLastError(); switch(tt) { case 10035: break; default: UpdateData(false); closesocket(this->m_hSocket); this->OnInitDialog(); this->OnButton2(); } } (7)编写发送函数 void CClientDlg::OnButton1() { UpdateData(TRUE); m_strShow += "\r\n"; m_strShow += m_strMsg; send(m_hSocket,m_strMsg.GetBuffer(1),m_strMsg.GetLength(),0); UINT tt=WSAGetLastError(); if(tt!=0) { m_strShow+="\r\n发送失败!"; } m_strMsg.Empty(); UpdateData(FALSE); } (8)重写OnDestroy()函数 void CClientDlg::OnDestroy() { CDialog::OnDestroy(); WSAAsyncSelect(m_hSocket, this->m_hWnd, 0, 0);//取消异步选择模式WSACleanup();//关闭套接字,终止连接释放资源 } (9)编写消息处理函数 long CClientDlg::OnClientMsg(WPARAM wParam, LPARAM lParam) { char buf[1024]; int len; switch(lParam) { case FD_CONNECT: m_strShow = "连接到服务器……"; UpdateData(FALSE); return 0; case FD_READ: len = recv(m_hSocket, buf, 1024, 0); buf[len]=NULL; m_strShow += "\r\n"; m_strShow += buf; UpdateData(FALSE); return 0; case FD_WRITE: return 0; case FD_CLOSE: m_strShow += "\r\n服务器关闭"; closesocket(m_hSocket); UpdateData(FALSE); return 0; default: m_strShow+="\r\n找不到服务器,请确认服务器IP和端口号是否正确"; closesocket(m_hSocket); UpdateData(FALSE); return 0; } } 3、应用程序测试。 (1) 当服务器不存在,或网络未连接时启动客户端并连接,会返回连接失败的信息。如图五所示。 图五客户端连接不成功 (2) 服务端创建成功,多个客户端创建成功,建立连接成功。如图六,图七所示。 图六成功创建的服务器 图七成功创建并连接到服务器的客户端