您的位置:首页 > 编程语言

3、Winsock编程详解

2016-01-23 01:11 113 查看
使用TCP创建网络应用程序稍微复杂一些,因为TCP是面向连接的协议,需要通信双方首先建立一个连接。 本节先以建立简单的TCP客户端和服务器端应用程序为例,详细说明WInsock的编程流程, 然后再介绍较为简单的UDP编程。

3.1 Winsock编程流程

使用Winsock编程的一般步骤是比较固定的, 可以结合后面的例子程序来理解他们。

1、套接字的创建和关闭

使用套接字之前,必须调用socket函数创建一个套接字对象,此函数调用成功将返回套接字句柄。

SOCKET socket(
int af,			// 用来制定套接使用的地址格式, WInsock中只支持AF_INET
int type,		// 用来制定套接字的类型
int protocol	// 配合type参数使用,用来制定使用的协议类型。 可以是IPPROTO_TCP等
);


type参数用来指定套接字的类型。 套接字有流套接字,数据报套接字和原始套接字等, 下面是常见的集中套接字类型定义。

SOCK_STREAM 流套接字,使用TCP提供有连接的可靠的传输

SOCK_DGRAM 数据报套接字,使用UDP提供无连接的不可靠的传输

SOCK_RAW 原始套接字,Winsock借口并不使用某种特定的协议去封装它,而是由程序自行处理数据报以及协议首部

当type参数制定为SOCK_STREAM和SOCK_GDRAM时,系统已经明确使用TCP和UDP来工作, 所以protocol参数可以制定为0。

函数执行失败返回INVALID_SOCKET 即 -1,可以通过调用WSAGetLastError取得错误代码。

也可以使用Winsock2的新函数WSASocket来创建套接字,与socket相比,它提供了更多的参数, 如可以自己选择下层服务提供者、设置重叠标志等,后面再具体讨论它。

当不使用socket创建的套接字时,应该调用closesocket函数将它关闭。 如果没有错误发生, 函数返回0, 否则返回SOCKET_ERROR。 函数用法如下:

int closesocket(SOCKET s);	// 函数唯一的参数就是要关闭的套接字句柄


3.2 绑定套接字到制定的IP地址和端口号

为套接字关联本地地址的函数是bind,用法如下:

int bind(
SOCKET s,							// 套接字句柄
const struct sockaddr* name,		// 要关联的本地地址
int namelen							// 地址的长度
);
bind函数用在没有建立连接的套接字上, 它的作用是绑定面向连接的或者无连接的套接字。 套接字被socket函数创建以后,存在与制定的地址家族里,但它是未命名的。bind函数通过安排一个本地名称到未命名的socket而建立此socket的本地关联。 本地名称包含3部分:主机地址,协议号(分别是UDP或TCP)和端口号。

本节的TCPServer程序使用以下代码绑定套接字s到本地地址。

SOCKET sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567);
sin.sin_addr.S_un.S_addr = INADDR_ANY;

// 绑定一个套接字到一个本地地址
if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf("Failed bind()\n");
return 0;
}


sockaddr_in 结构中的sin_family 字段用来制定地址家族,该字段和socket函数中的af参数的含义相同, 所以唯一可以使用的就是AF_INET。 sin_port字段和sin_addr字段分别制定套接字需要绑定的端口号和IP地址。 放入这两个字段的数据的字节顺序必须是网络字节顺序。因为网络字节顺序和Intel CPU的字节顺序刚好相反,所以必须首先使用htons函数进行转换。

如果应用程序不关系所使用的地址,可以制定Internet地址为INADDR_ANY,指定端口号为0. 如果Internet地址等于INADDR_ANY,系统会自动使用当前主机配置的所有IP地址,简化了程序设计;如果端口号等于0,程序执行时系统会为这个应用程序分配唯一的端口号,其值在1024~5000之间。 应用程序可以在bind之后使用getsockname来知道为它分配的地址。但是要注意,直到套接字连接上之后getsockname才可能填写Internet地址,因为对一个主机来说可能有多个地址是可用的。

TCP客户端程序也可以在不显式绑定地址和端口号的情况下发送数据或者连接。在这种情况下,系统也会默认地为套接字绑定一个本地端口(1024~5000)之间。

3.3 设置套接字进入监听状态

listen函数设置套接字进入监听状态

int listen(
SOCKET s,			// 套接字句柄
int backlog			// 监听队列中允许保持的尚未处理的最大连接数量
);
为了接收连接, 首先使用socket函数创建套接字, 然后使用bind函数将它绑定到本地地址,使用listen函数为到达的连接制定backlog, 最后使用accept接收请求的连接。

listen仅应用在支持连接的套接字上, 如SOCK_STREAM类型的套接字。函数执行成功后,套接字s进入了被动模式,到来的连接会被通知要排队等待接受处理。

在同一时间处理多个连接请求的器通常使用listen函数,如果一个连接请求到达,并且排队已满,客户端将接收到WSAECONNREFUSED错误。

3.4 接受连接请求

accept函数用于接受到来的连接。

SOCKET accept
{
SOCKET s,					// 套接字句柄
struct sockaddr* addr,		// 一个指向sockaddr_in结构的指针,用于取得对方的地址信息
int* addrlen				// 一个指向地址长度的指针
};
第一个参数s是此连接使用的客户端套接字, 另两个参数name和namelen用来寻址远程套接字(正在监听的服务器套接字)。

3.5 收发数据

对流套接字来说,一般使用send和recv函数来收发数据。

int send(
SOCKET s,				// 套接字句柄
const char FAR* buf,	// 要发送数据的缓冲区地址
int len,				// 缓冲区长度
int flags				// 指定了调用方式,通常设为0
);
int recv(SOCKET s, char FAR* buf, int len, int flags);
send函数在一个连接的套接字上发送缓冲区内的数据,返回发送数据的实际字节数。recv函数从对方接收数据,并将其存储到指定的缓冲区。 flags参数在这两函数中通常设为0.

在阻塞模式下,send将会阻塞线程的执行知道所有的数据发送完毕(或者发生错误),而recv函数将返回尽可能多的当前可用信息, 直到达到缓冲区制定的大小。

典型的过程图

TCP服务器程序和客户程序的创建过程如图2.2所示。 服务器端创建监听套接字,并为它关联一个本地地址(指定IP地址和端口号),然后进入监听状态准备接收客户端的连接请求。为了接收客户端的连接请求,服务器端必须调用accept函数。

客户端创建套接字后即可通过connect函数去试图连接服务器监听套接字。 当服务端的accept函数返回后,connect函数也返回。 此时客户端使用socket函数创建的套接字,服务器端使用accept函数创建的套接字,双方就可以通信了。

下图是一个典型的服务端与客户端通信示意图



TCP服务器和客户端程序举例

下面是最简单的TCP服务器程序和TCP客户端程序的例子。 这两个程序都是控制台界面的Win32应用程序。

运行服务器程序, 如果没有错误发生,将在本地机器上的4567端口上等待客户端的连接。 如果没有连接请求,服务器会一直处于休眠状态。

运行服务器之后,再运行客户端程序, 首先客户端连接到服务器, 双方套接字可以通信。

服务端

#pragma once
#include "../common/initsock.h"  // 这里注意你自己需要初始化一下socket库
#include <iostream>

CInitSock initSock;			// 初始化Winsock库

int main()
{
// 创建套接字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if ( sListen == INVALID_SOCKET)
{
printf("Failed socket()\n");
return 0;
}

// 填充socketaddr_in 结构
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567);
sin.sin_addr.S_un.S_addr = INADDR_ANY;

// 绑定这个套接字到一个本地地址
if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf("Failed bind()\n");
return 0;
}

// 进入监听模式
if (::listen(sListen,2) == SOCKET_ERROR)
{
printf("Failed listen()\n");
return 0;
}

// 循环接受客户端的连接请求
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
SOCKET sClient;
char szText[] = "TCP Server Demo! \r\n";

while (true)
{
// 接收新连接
sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
if ( sClient == INVALID_SOCKET)
{
printf("Failed accept()");
continue;
}
printf("接收到一个连接:%s\r\n", inet_ntoa(remoteAddr.sin_addr));

// 向客户端发送数据
::send(sClient, szText, strlen(szText), 0);

// 关闭同客户端的连接
::closesocket(sClient);
}

// 关闭监听套接字
::closesocket(sListen);
//GetGlobalData();

return 0;
}


客户端

#include <iostream>
#include "../common/initsock.h"<span style="white-space:pre">			</span>// 这里要注意一下 需要先初始化Winsock库

CInitSock initSock;			// 初始化Winsock库

int main()
{
// 创建套接字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if ( s == INVALID_SOCKET)
{
printf("Failed socket()\r\n");
return 0;
}

// 也可以在这里调用bind函数绑定一个本地地址
// 否则系统将会自动安排
// ...

// 填写远程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(4567);
// 注意 这里要填写服务器程序的机器的ip地址
servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
printf("Failed connect()\n");
return 0;
}

// 接收数据
char buff[256] = { 0 };
int nRecv = ::recv(s, buff, 256, 0);
if ( nRecv > 0 )
{
buff[nRecv] = '\0';
printf("接收到数据:%s", buff);
}

// 关闭套接字
::closesocket(s);

getchar();
return 0;
}


结果如图所示

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: