(转)Windows sockets编程指南

以下是Windows Sockets编程的入门指南,提供帮助理解最基础的Winsock函数和数据结构,以及它们是如何一起运作。

零、关于服务器和客户端

有两种截然不同网络应用程序:服务器(Servers)和客户端(Clients)。服务器和客户端有着不同的行为,因此建立它们的过程也是不一样的,下面是建立一个TCP/IP服务器和客户端的一般模型。
服务器
1. 初始化Winsock
2. 建立一个套接字(Socket)
3. 绑定Socket
4. 在Socket上为客户端监听
5. 接受(Accept)一个来自客户端的连接
6. 接收和发送数据
7. 断开连接
客户端
1. 初始化Winsock
2. 建立一个Socket
3. 连接到服务器
4. 发送和接收数据
5. 断开连接
注意:对于二者来说有几步是一样的,这几步实现起来几乎完全一样,指南中这几步会具体的根据所建立程序的类型说明的。

一、建立一个基础的 Winsock程序

建立一个基础的 Winsock程序
1. 建立一个空的工程
2. 添加一个空的C++源文件
3. 确保构建环境对微软SDK的Lib、Include和Src的文件夹正确引用
4. 确保构建环境的连接器依赖项包含了WS2_32.lib,使用Winsock的程序必须连接此文件
5. 开始进行Winsock编程,通过包含Winsock2.h来使用Winsock的API。(Winsock2.h头文件已经包含了大部分Winsock函数、数据结构和定义,Ws2tcpip.h头文件包含了在Winsock2协议兼容文档中为TCP/IP用于检索IP地址的新函数和数据结构。

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
int main() {
    return 0;
}<br>

注意:如果需要使用IP Helper APIs的话需要引用Iphlpapi.h头文件,当Iphlpapi.h被引用的时候,引用Winsock2.h的#include引用行应置于Iphlpapi.h行之上。Winsock2.h头文件已经内在的引用了 Windows.h的核心元素,所以通常在 Winsock程序中不包含 Windows.h文件。如果需要包含Windows.h头文件,应定义#define WIN32_LEAN_AND_MEAN宏。因为一些历史原因,Windows.h包含有1.1版的Winsock.h头文件,所以Winsock.h和Winsock2.h会因为定义而冲突。WIN32_LEAN_AND_MEAN宏阻止了Winsock.h被Windows.h包含进来。下面是例子:

 #ifndef WIN32_LEAN_AND_MEAN
 #define WIN32_LEAN_AND_MEAN
 #endif
 #include <windows.h>
 #include <winsock2.h>
 #include <ws2tcpip.h>
 #include <iphlpapi.h>
 #include <stdio.h>
int main() {
    return 0;
}

二、初始化 Winsock

所有的进程(程序和 DLLs)在使用Winsock函数之前必须用一些函数来使用 Windows Sockets DLL,这也需要弄清楚Winsock在系统上是被支持的。(CB版:所有的Winsock程序必须初始化来确保Windows socket被系统支持。)
初始化Winsock
1. 建立一个WSADATA对象,叫做wsaData
WSADATA wsaData;
2. 调用WSAStartup,并返回一个整数值,使用它来查错

int iResult;
//初始化Winsock
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != 0) {
    printf(“WSAStartup 失败:%d\n”, iResult);
    return 1;
}

WSAStartup函数被调用来使用WS2_32.lib,WSADATA结构体包含着有关 Winsock的实现信息,参数MAKEWORD(2,2)向系统声明使用2.2版本的Winsock,从而使得高版本的Winsock可以使用。

三、建立一个 Socket

MSDN和C++Builder的文档在这里就分道扬镳了,虽然内容基本一样,但结构不同。以下以MSDN为主。

(一)客户端版

初始化之后,一个SOCKET对象必须被客户端实例化,建立一个 Socket
1.声明一个addrinfo对象,它包含着一个 sockaddr结构,并且初始化这些值。在这个程序中,互联网地址族(Internet address family)没有明确的指出返回IPv6还是IPv4。程序请求Socket的类型是一个TCP协议 流套接字(stream socket)。

#define DEFAULT_PORT "27015"
struct addrinfo *result = NULL,
                 *ptr = NULL,
                 hints;
ZeroMemory( &hints, sizeof(hints) );hints.ai_family = AF_UNSPEC;//未指明返回类型
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;

2.调用getaddrinfo函数获取从命令行传输来的服务器名字的IP地址,getaddrinfo函数返回一个整数用来查错
//分析服务器地址和端口

iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
if ( iResult != 0 ) {
     printf("getaddrinfo 失败: %d\n", iResult);
     WSACleanup();
     return 1;
}

3.建立一个叫ConnectSocket的SOCKET对象

SOCKET ConnectSocket = INVALID_SOCKET;

4.调用socket函数,将返回值赋予ConnectSocket。在本程序中,使用通过调用getaddrinfo得到的hints参数中的第一个IP地址,hints中包含了地址族、套接字类型和指定的协议类型。在本例中 TCP 流套接字被指定为 SOCK_STREAM,协议类型被指定为IPPROTO_TCP。地址族是未指定的(AF_UNSPEC),所以服务器的返回值可能是IPv4或IPv6。如果客户端想只使用IPv6或IPv4,hints参数中的地址族应设置为AF_INET6或AF_INET。

//试图用调用getaddrinfo返回的第一个地址连接
ptr = result;
//建立一个SOCKET来连接服务器
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);

5.检查错误确保这个socket是有效的

if (ConnectSocket == INVALID_SOCKET) {
    printf(“调用socket时发生错误:%d\n”, WSAGetLastError());
    freeaddrinfo(result);
    WSACleanup();
    return 1;
}

socket函数的参数可以根据不同的需要而改变。检错是一个成功的网络程序的关键,如果socket函数调用失败,它会返回INVALID_SOCKET,所以上面的if语句被用来抓住在建立套接字时任何可能发生的错误。WSAGetLastError返回最后发生的错误的代码。
注意 大量的检错取决于你的程序,WSACleanup用于停止WS2_32.DLL的使用

(二)服务器版

初始化后,套接字必须被服务器实例化
为服务器建立一个 socket
1. getaddrinfo函数用来确定sockaddr结构中的值:
AF_INET表示IPv4地址族
SOCK_STREAM表示一个流套接字(stream socket)
IPPROTO_TCP表示使用TCP协议
AI_PASSIVE 标志指出调用者将要在 bind 函数中使用返回的套接字地址结构体。当 AI_PASSIVE 标志被设置,而getaddrinfo函数的参数nodename是NULL指针的时候,如果是IPv4地址,套接字的IP地址部分将被设置成INADDR_ANY,而IPv6将会是IN6ADDR_ANY_INIT。27015是服务器将要和客户端连接的端口

#define DEFAULT_PORT "27015"
struct addrinfo *result = NULL,
    *ptr = NULL,
    Hints;
ZeroMemory( &amp;hints, sizeof(hints) );
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
//分析在服务器已使用的本地地址和端口
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if ( iResult != 0 ) {
    printf(“getaddrinfo调用失败:%d\n”, iResult);
    WSAClearnup();
    return 1;
}

2.为服务器端建立一个名为ListenSocket的SOCKET对象用来监听客户端的连接

SOCKET ListenSocket = INVALID_SOCKET;

3.调用socket函数并将返回值赋予ListenSocket。对于这个服务器程序使用第一个由getaddrinfo返回IP地址,其中参数hints包含了地址族、连接类型和协议类型。在本例中一个IPv4的TCP流套接字被建立,类型是SOCK_STREAM,协议是IPPROTO_TCP。所以ListenSocket返回IPv4地址。
如果服务器程序想要使用IPv6监听,参数hints中地址族应该设为AF_INET6,如果服务器想要同时监听IPv4和IPv6,就必须建立两个socket,一个监听IPv4,一个监听IPv6。两个套接字必须被程序独立分开处理。
Windows Vista以及其后版本提供了双重方法来建立一个单独的IPv6套接字来同时监听Ipv4和IPv6。有关此特性的更多信息,参看MSDN上“Dual-Stack Sockets”。

//在服务器建立一个socket用来监听客户端连接
LisitenSocket = socket( result->ai_family, result->ai_socktype, result->ai_protocol);

4.检错确保此套接字是有效的

if (ListenSocket == INVLID_SOCKET) {
    printf(“Error at socket(): %ld\n”, WSAGetLastError());
    freeaddrinfo(result);
    WSACleanup();
    return 1;
}

四、数据交换前的行为

(一)客户端版:连接 Socket

为了客户端能够通过网络交流,它必须连接到一个服务器。
连接到 Socket调用connect函数,将已建立的socket和一个sockaddr结构体作为参数,然后检错。

//连接到服务器<br>
iResult = connect( ConnectSocket, ptr->ai_addr, (int)pter->ai_addrlen);
if (iResult == SOCKET_ERROR) {
    closesocket(ConnectSocket);
    ConnectSocket = INVALID_SOCKET;
}
//如果连接失败,将会尝试getaddrinfo返回的下一个地址
//但在这个简单的例子中,我们只是释放由getaddrinfo返回的资源并且打印错误信息
freeaddrinfo(result);
if (ConnectSocket == INVALID_SOCKET) {
    printf(“无法连接到服务器!\n”);WSACleanup();
    return 1;
}

函数getaddrinfo用来确定socketaddr结构中的值,在本例中由 getaddrinfo返回的第一个IP地址被用来表述传递到 connect函数的sockaddr结构体,如果connect调用第一个IP地址失败,尝试由getaddrinfo返回的链表的下一个addrinfo结构体。
在sockaddr结构体中描述的信息包括:
1 客户端尝试连接的服务器的IP地址
2 客户端将要连接的服务器端口,像上面的27015就是客户端调用getaddrinfo函数时的端口

(二)服务器端版:绑定 Socket、监听端口、接受连接

为了服务器能够接受来自客户端的连接,它一定要有一个网络地址。下面的代码展示了如何绑定一个已经由IP地址和端口创建的套接字,客户端通过IP地址和端口来连接到主机网络。绑定一个 Socket,调用bind函数,将已经建立的socket和从getaddrinfo函数返回的sockaddr作为参数传入,然后检错。

//设置TCP监听套接字
iResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
    printf(“绑定失败:%d\n”, WSAGetLastError());
    freeaddrinfo(result);
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
}

监听一个 Socket
在绑定了Socket之后,服务器一定要为来到的客户端请求监听IP地址和端口。调用listen函数,将建立好的ListenSocket和backlog的值——表示待定的需要接受的连接队列的最大数,作为参数传入。在本例中,参数backlog被设为SOMAXCONN。这个特别的常数告诉Winsock提供给这个socket允许的最大待连接队列数。然后检错。

if ( listen( ListenSocket, SOMAXCONN ) == SOCKET_ERROR ) {
    printf(“监听错误:%d\n”, WSAGetLastError());
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
}

接受连接
一旦套接字开始监听连接,程序就必须处理套接字上的连接请求接受套接字的连接
1. 为接受来自客户端的连接建立一个临时的SOCKET对象叫做ClientSocket
SOCKET ClientSocket;
2. 通常服务器程序会通过listen函数建立一个持续的循环来检测连接请求。如果发生了连接请求,调用accept函数处理这个连接。注意在这个基础例子中,代码只接受一个连接。

ClientSocket = INVALID_SOCKET;
//接受一个客户端套接字
ClientSocket = accept(ListenSocket, NULL, NULL);
if ( ClientSocket == INVALID_SOCKET) {
    printf(“接受连接失败:%d\n”, WSAGetLastError());
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
}

3.当客户端连接被接受之后,服务器通常将接受的连接转移到另一个工作的线程或I/O工作端口,并继续接受其他连接。在这个基础例子中,服务器将会直接进行下一步。

五、数据交换

(一)客户端版:发送和接收数据

下面的代码展示了一旦连接完成,客户端如何使用send和recv函数。

#define DEFAULT_BUFF 512
char *sendbuf = “this is a test”;
char recvbuf[DEFAULT_BUFF];
int recvbuflen = DEFAULT_BUFF;
int iResult;
//发送数据
iResult = send ( ConnectSocket, sendbuff, (int)strlen(sendbuff) );
if ( iResult == SOCKET_ERROR ) {
    printf(“Send error: %d\n”, WSAGetLastError());
    closesocket(ConnectSocket);
    WSACleanup();
    return 1;
}
printf(“已经发送:%ld\n”, iResult);
//一旦没有更多数据需要发送,关闭发送连接,客户端仍然可以用ClientSocket来接收数据
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
    printf(“关闭发送连接失败:%d\n” WSAGetLastError());
    closesocket(ConnectSocket);
    WSACleanup();
    return 1;
}
//接受数据,知道服务器关闭连接
do {
    iResult = recv(ConnectSocket, recvbuff, recvbuflen, 0);
     if ( iResult &gt; 0 )
        printf(“接收到字节:%d\n”, iResult);
    else if ( iResult == 0 )
        printf(“连接关闭\n”);
    else
        printf(“recv失败:%d\n”, WSAGetLastError());
} while ( iResult > 0 );

函数send和recv的返回值已发送或已接受的字节数目值,或不同的错误。两个函数都有相同的参数:活动的socket,一个char类型
的缓冲区,发送或接受的字节长度,或任何可以使用的标志。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Windows Sockets网络编程》是WindowsSockets网络编程领域公认的经典著作,由Windows Sockets2.0规范解释小组负责人亲自执笔,权威性毋庸置疑。它结合大量示例,对WindowsSockets规范进行了深刻地解读,系统讲解了WindowsSockets网络编程及其相关的概念、原理、主要命令、操作模式,以及开发技巧和可能的陷阱,从程序员的角度给出了大量的建议和最佳实践,是学习WindowsSockets网络编程不可多得的参考书。   全书分为三部分:第一部分(第1~6章),提供了翔实的背景知识和框架方面的概念,借助于此框架,读者可理解WinSock的具体细节,包括WindowsSockets概述、OSI网络参考模型、TCP/IP协议簇中的协议和可用的服务、WinSock网络应用程序的框架及其工作机制、WinSock的三种操作模式、socket通信机制等;第二部分(第7~12章),以FTP客户端实例为基础介绍了函数实例库,还介绍了客户端程序、服务器程序和DLL中间构件及它们的相应函数,并涵盖socket命令和选项及移植BSDSockets相关事项等;第三部分(第13~17章),介绍了应用程序调试技术和工具,针对应用编程中的陷阱的建议和措施,WinSockAPI的多种操作系统平台,WinSock规范的可选功能和WinSock规范2.0中的所有新功能。 译者序 序 前言 第1章 Windows Sockets概述 1.1 什么是Windows Sockets 1.2 Windows Sockets的发展历史 1.3 Windows Sockets的优势 1.3.1 Windows Sockets是一个开放的标准 1.3.2 Windows Sockets提供源代码可移植性 1.3.3 Windows Sockets支持动态链接 1.3.4 Windows Sockets的优点 1.4 Windows Sockets的前景 1.5 结论 第2章 Windows Sockets的概念 2.1 OSI网络模型 2.2 WinSock网络模型 2.2.1 信息与数据 2.2.2 应用协议 2.3 WinSock中的OSI层次 2.3.1 应用层 2.3.2 表示层 2.3.3 会话层 2.3.4 传输层 2.3.5 网络层 2.3.6 数据链路层 2.3.7 物理层 2.4 模块化的层次框 2.5 服务和协议 2.6 协议和API 第3章 TCP/IP协议服务 3.1 什么是TCP/IP 3.2 TCP/IP的发展历史 3.3 传输服务 3.3.1 无连接的服务:UDP 3.3.2 面向连接的服务:TCP 3.3.3 传输协议的选择:UDP与TCP的对比 3.4 网络服务 3.4.1 IP服务 3.4.2 ICMP服务 3.5 支持协议和服务 3.5.1 域名服务 3.5.2 地址解析协议 3.5.3 其他支持协议 3.6 TCP/IP的发展前景 第4章 网络应用程序工作机制 4.1 客户端-服务器模型 4.2 网络程序概览 4.3 socket的打开 4.4 socket的命名 4.4.1 sockaddr结构 4.4.2 sockaddr_in结构 4.4.3 端口号 4.4.4 本地IP地址 4.4.5 什么是socket名称 4.4.6 客户端socket名称是可选的 4.5 与另一个socket建立关联 4.5.1 服务器如何准备建立关联 4.5.2 客户端如何发起一个关联 4.5.3 服务器如何完成一个关联 4.6 socket之间的发送与接收 4.6.1 在“已连接的”socket上发送数据 4.6.2 在“无连接的”socket上发送数据 4.6.3 接收数据 4.6.4 socket解复用器中的关联 4.7 socket的关闭 4.7.1 closesocket 4.7.2 shutdown 4.8 客户端和服务器概览 第5章 操作模式 5.1 什么是操作模式 5.1.1 不挂机,等待:阻塞 5.1.2 挂机后再拨:非阻塞 5.1.3 请求对方回拨:异步 5.2 阻塞模式 5.2.1 阻塞socket 5.2.2 阻塞函数 5.2.3 伪阻塞的问题 5.2.4 阻塞钩子函数 5.2.5 阻塞情境 5.2.6 撤销阻塞操作 5.2.7 阻塞操作中的超时 5.2.8 无最少接收限制值 5.2.9 代码示例 5.3 非阻塞模式 5.3.1 怎样使socket成为非阻塞的 5.3.2 成功与失败不是绝对的 5.3.3 探询而非阻塞 5.3.4 显式地避让 5.3.5 代码示例 5.4 异步模式 5.4.1 认识异步函数 5.4.2 撤销异步操作 5.4.3 代码示例 5.4.4 AU_T

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值