1. Winsock简介 - windows网络编程
Winsock是一种标准API. 主要用于网络通信.
这些API函数有Winsock1和Winsock2版本. 通过前缀WSA区分.
1.1 Winsock头文件及库文件
使用Winsock2规范时 应包含winsock2.h 连接的库为 WS2_32.LIB
使用Winsock1规范时. 应包含winsock.h 连接的库为 WSOCK32.LIB
另外mswsock.h用于微软专用编程扩展. 连接的库为 MSWSOCK.DLL
1.2 Winsock的初始化
每个Winsock应用程序都必须加载合适的Winsock DLL版本. 函数如下:
int WSAStartup {
WORD wVersionRequested, //加载的Winsock库的版本.高位字是次版本.低位是主版本.
//可以用MAKEWORD(x,y)宏来方便的指定x.y版本
LPWSADATA 1pWSAData, //指向WSAData结构(见下边)的指针. 本函数将填充这个结构体.
WORD wVersionRequested,
LPWSADATA 1pWSAData );
其中WSAData结构如下:
typedef struct WSAData{
WORD wVersion; //WSAAtartup函数会将这个字段设置为将要使用的Winsock版本.
WORD wHighVersion; //现有Winsock库的最高版本.
char szDescription[WSADESCRTIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;//客同时打开的最大套接字数.取决与可用的物理资源.
unsigned short iMaxUdpDg; //数据报最大长度
char FAR* 1pVendorInfo; //保留字段
}WSADATA,*LPWSADATA ;
从windows98向后的平台都支持Winsock2.2版本. 但Windows CE支持的是Winsock 1.1版本.
如果WSAStartup()的第一个参数wVersionRequested 指定的版本比当前平台上的最新版本还新.则函数
就会失败.
在使用Winsock接口编程完成后.要调用
int WSACleanup(void);
来释放所有由Winsock分配的资源.
1.3 错误检查和处理
对Winsock函数来说返回错误是很常见的. 失败时最常见的返回值是SOCKET_ERROR(值为-1).
检查到错误后. 可以用
int WSAGetLastError(void);
函数返回int. 这是在WINSOCK1.h或Winsock2.h中定义的常量值.来获得一段代码专门说明错误.
与WSAGetLastError函数对应的有个WSASetLastError().
它可以手动设置一个错误代码让WSAGetLastError去获取.
例如下边的框架:
#include<winsock2.h>
int main (){
WSADATA wsaData;
//初始化Winsock版本2.2
if ((WSAStartup(MAKEWORD(2,2),&wsaData))!= 0){
// 因为这个函数调用时Winsock还没有加载. 所以不能用WSAGetLastError()来确定错误.
printf("WSAStartup fa土led with error %d \n",Ret);
return 1;
}
//当应用程序结束调用WSACleanup之后,设置Winsock通信代码
if (WSACleanup() == SOCKET ERROR){
printf("WSACleanup fa工led with error %d \n",WSAGetLastError());
}
return 0;
}
1.4 协议寻址
本章介绍使用IPv4协议建立Winsock通信的基本知识.
IPv4寻址
应用程序通过SOCKADDR_IN结构来指定IP地址和服务器端口信息:
struct sockaddr_in
{
short sin_family; //设置为 AF_INET 表示Winsock正使用IP地址族.
u_short sin_port; //通信端口
struct in_addr sin_addr; //这个字段用sin_addr(4字节) 保存IP地址.
char sin_zero[8]; //这个字段是填充项.使这个结构体大小和SOCKADDR结构的一样.
};
IP地址的表示
在unsigned long 和点分十进制字符串之间转换IP地址的表示可以用下边的函数:
unsigned long inet_addr( const char FAR* cp);
指定IP地址和端口号时.必须用"网络字节序", 这和平时用的字节序是相反的.
将一个主机字节序的数转换为网络字节序的数需要用下边的函数转换:
u_long htonl(u_long hostlong);
int WSAHtonl(SOCKET s, u_long hostlong, u_long FAR* lpnetLong);
u_short htons(u_short hostshort);
int WSAHtons(SOCKET s, u_short hostshort, u_short FAR* lpnetshort);
对应的将网络字节序转换成主机字节序的用下边的四个:
ntohl WSANtohl ntohs WSANtohs
1.5 创建套接字
套接字是SOCKET类型. 创建套接字的函数是 socket 和 WSASocket.
SOCKET socket(int af, int type, int protocol);
其中:
af是协议的地址族. 如IPv4对应的是 AF_INET
type是协议的套接字类型.如果是TCP/IP则是 SOCK_STREAM 如果是UDP/IP则是 SOCK_DGRAM
protocol用于给定的地址和套接字类型具有多重入口时.对具体的传送做限定.对于TCP应将该字段设
为 IPPROTO_TCP 对于UDP则设为 IPPROTO_UDP
1.6 面向连接的通信
即通过TCP/IP协议来通信.
1.6.1 服务器API函数
这里说的服务器时一个进程.它等待任意数量的客户机来与之建立连接.
将一个套接字绑定到一个已知的名称(即第2个参数.指定IP地址和端口)上.通过bind函数来完成.
int bind(SOCKET s, const struct sockaddr FAR* name, int namelen);
参数为:
s 带绑定的套接字
name 名称
namelen 指出第2个参数指向的对象的长度
例如:
SOCKET s;
SOCKADDR_IN tcpaddr; //地址结构.结构的定义见1.4节
int port = 5150;
//创建套接字
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//设置地址结构
tcpaddr.sin_family = AF_INET; //AF_INET表示使用IP地址族
tcpaddr.sin_port = htons(port); //端口.用网络字节序的u_short表示
tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址.同样是网络字节序.可以是一个
//显示指定的IP地址.也可以像INADDR_ANY这样.表示所有的可用接口上(一个主机可以有多IP).
//将套接字和地址结构绑定
bind(s, (SOCKADDR*)&tcpaddr, sizeof(tcpaddr)); //注意第2个参数的类型有转换.
// 这个函数如果出错会返回SOCKET_ERROR .
// 常见的错误是: WSAEADDRINUSE(表示本地这个端口和IP已被另一个进程绑定).
// WSAEFAULT (表示该套接字s已被绑定过了)
现在套接字已被绑定到本地一个端口上了. 接着要让他进入监听模式.使用函数listen:
int listen(SOCKET s, int backlog);
其中: s是已被bind过的套接字. backlog则是指定被搁置的连接队列的最大长度.当连接的客户数大于这里的
最大长长度并且并且服务进程没有来得及处理. 则多出的连接请求会失败.
这个函数相关的错误.常见的有 WSAEINVAL(套接字在listen之前没有bind).
现在可以让服务器接受连接.使用函数accept, WSAAccept, AcceptEX. 本章先看accept.如下:
SOCKET accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
其中
s是处于监听模式的套接字. addr是SOCKADDR_IN对象的地址. addrlen是第2个参数的长度.
这个函数取出被搁置的连接队列中的第一个连接请求.对它服务.函数执行后第2个参数和第3个参数指向的变量
会被函数设置为客户IPv4地址信息和该信息的长度. 并且.accept函数返回一个新的套接字.对该客户的所有
后续操作(如数据收发等)都用这个新套接字来完成. 而原来的套接字s则继续处于监听模式.
如果这个函数出错.会返回 INVALID_SOCKET . 在书后会讲.
最后.还要关闭套接字. 这等会会讲.
现在看一个tcp/ip服务器的完整例子(去掉了错误检查):
#include <winsock2.h>
void main()
{
WSADATA wsaData;
SOCKET ListeningSocket;
SOCKET NewConnection;
SOCKADDR_IN ServerAddr;
SOCKADDR_IN ClientAddr;
int Port = 5150;
//初始化Winsock版本2.2
WSAStartup(MAKEWORD(2,2), &wsaData);
//创建一个套接字并让他监听客户的连接
ListeningSock = sock(AF_INET, SOCK_STREAM, IPPROTO_TCP);
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = htons(Port);
ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(ListeningSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
lesten(ListeningSocket, 5);
//接受一个新客户连接
NewConnection=accept(ListeningSocket,(SOCKADDR*)&ClientAddr, &ClientAddrlen));
//可以在NewConnection上进行和该客户通信.也可以继续在ListeningSocket上接受其他客户连接.
//关闭套接字
closesocket(NewConnection);
closesocket(ListeningSocket);
WSACleanup();
}
1.6.2 客户端API函数
在客户机上建立连接的步骤:
1. 创建套接字.
2. 建立SOCKADDR地址结构.(指定要连接到的服务器的IP地址和端口号)
3. 用connect或WSAConnect初始化客户与服务器的连接.
其中.前两步和服务器端建立连接的一样. 第3步使用的函数如:
int connect(SOCKET s, const struct sockaddr FAR* name, int namelen);
其中:
s就是将要和服务器建立连接的套接字.
name是服务器的地址结构(SOCKADDR_IN).
namelen是name的长度.
如果函数失败.一般返回的错误有:
WSAECONNREFUSED 表示服务器没有监听这个端口
WSAETIMEDOUT 一般表示主机不可到达.
例如:
#include <winsock2.h>
void main(){
WSADATA wsaData;
SOCKET s;
SOCKADDR_IN ServerAddr;
int port = 5150;
//初始化为 winsock2.2版
WSAStartup(MAKEWORD(2,2), &wsaData);
//创建新的套接字
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//建立SOCKADDR_IN结构. 用来表示服务器的IP和端口.
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = htons(Port);
ServerAddr.sin_addr.s_addr = inet_addr("136.149.3.29");
//用套接字s创建一个到服务器的连接.
connect(s, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
//使用套接字s传递数据.
//关闭s
closesocket(s);
//
WSACleanup();
}
TCP客户端的状态:
每个套接字的初始状态都是CLOSED.
客户机初始化一个连接.向服务器发出连接请求SYN包时(第1次握手).客户机套接字的状态为SYN_SENT.
然后客户机等待一个从服务器端返回的SYN_ACK包应答(第2次握手).
如果收不到这个SYN_ACK包. 客户机会超时.回到CLOSED状态.
如果收到. 则客户机再发送一个ACK包给服务器作为响应(第3次握手).客户机套接字状态变为ESTABLISHED.
TCP服务端的状态:
一开始套接字的初始状态都是CLOSED.
套接字和本地接口(IP)端口绑定之后(通过bind函数).再通过lesten函数进入侦听状态.这时服务器套接字
状态为 LISTEN.
当服务器收到客户机的连接请求时.要发送一个SYN_ACK包回应客户机(第2次握手).这时状态为SYN_RCVD.
然后客户机再返回ACK包(第3次握手).服务器收到后.状态变为 ESTABLISHED.
关闭套接字时:
如果是主动关闭.即在ESTABLISHED状态下向对方发送一个FIN包(通过closesocket函数).本端的状态就变为
FIN_WAIT_1 , 收到对方返回的ACK后状态变为FIN_WAIT_2. 然后等接收到对方发来的FIN包时.本端会发送
一个ACK回应. 并将本端的套接字设置为 TIME_WAIT(也叫2MSL等待状态). 再过2倍的MSL时间后将状态变为
CLOSED状态.
如果是被动关闭.即本端先收到对方发来的FIN包. 并用一个ACK包回应. 此时本端状态变为CLOSE_WAIT. 然后
如果本端也要结束传送. 会向对方发送FIN包. 这就使本端进入LAST_ACK状态. 接着会收到对方的ACK回应.
之后,本端状态变为 CLOSED状态.
1.6.3 数据传输
发送数据使用send和WSASend函数.
int send(SOCKET s, const char FAR* buf, int len, int flags);
函数用s发送buf中的len个字节的数据. flags可以为0. MSG_DONTROUTE. MSG_OOB.
如果成功.函数返回发送的字节数.否则返回SOCKET_ERROR.
再Winsock2版本也可以用WSASend函数.如下:
int WSASend( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
其中.
s是套接字.
lpBuffers是指向WSABUF结构的指针.每个WSABUF就代表一个缓冲区.它指向一个WSABUF对象.或一个数组.
dwBufferCount 指出第二个参数指向的数组中的对象数.
lpNumberOfBytesSent指向的DWORD.会在函数返回时被设置为已发送的字节数.
lpOverlapped和lpCompletionROUTine参数用于重叠I/O. 这以后会讲.
函数执行成功返回0. 否则返回SOCK_ERROR. 常见错误与send函数一样.
最后还有一个发送函数.这个函数很少用:
int WSASendDisconnect(SOCKET s, LPWSABUF lpOutboundDisconnectData);
接收数据使用recv和WSARecv函数.
int recv(SOCKET s, char FAR* buf, int len, int flags);
其中.
s是用来接受数据的套接字.
buf是接收数据的字符缓冲区.
len是准备接收的字节数或buf的缓冲区长度.
flags可以是 0. MSG_PEEK, MSG_OOB. 其中MSG_PEEK是表示查看消息, 即将系统缓冲区的数据复制到
buf里.但在系统缓冲区中还保留这些数据.并且将复制的字节数返回.
注意:在基于消息或基于数据报的套接字(如UDP)上使用recv函数时.如果收到的数据大于len.函数会先填满
buf.然后产生WSAEMSGSIZE错误.
但在流协议(如TCP)里则不存在这样的问题.即recv函数会尽量取出len个字节的数据.其它的则不取出.
在winsock2里还可以使用函数WSARecv来接收数据.如下;
int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_COUTINE lpCompletionRoutine);
另外也有一个不常用的接收函数:
int WSARecvDisconnect(SOCKET s, LPWSABUF lpInboundDisconnectData);
16.4 流协议
流协议(如TCP)中.发送者和接收者可以将数据分解或合并.
在流套接字上的收发函数不能保证要求读取或写入的数据量.例如:
char sendbuff[2048];
int nBytes = 2048;
ret = send(s, sendbuff, nBytes, 0);
ret赋值为已发送的字节数. 但ret可能小于2048字节.
系统会为每个收发数据的套接字分配缓冲区. 发送时先将数据从sendbuff放到系统缓冲区.这样系统缓冲区有
可能被填满. 系统缓冲区的数据在发送时会取出. 而且对于TCP/IP,如果接收端的窗口大小为0. 发送端就暂
时不能发送数据. 所以发送时会对数据分解.
为保证将所有字节都发出去. 可采用下边的代码:
char sendbuff[2048];
int nBytes = 2048 , nLeft, idx;
nLeft = nBytes;
idx = 0;
while (nLeft>0) {
ret = send(s, &sendbuff[idx], nLeft, 0);
if(ret = SOCKET_ERROR) chucuo...;
nLeft -= ret;
idx += ret;
}
这样就会一直发送完整个缓冲区的数据.
接受数据时应用程序通常不会关心应该读多少数据.
如果应用程序需要通过流协议获取离散消息.要像下边这样.
如果消息长度固定.如512字节.则:
char recvbuff[1024];
int ret, nLeft = 512, idx = 0;
while(nLeft > 0) {
ret = recv(s, &recvbuff[idx], nLeft, 0);
if (ret == SOCKET_ERROR) chucuo...
idx += ret;
nLeft -= ret;
}
如果消息长度不同,可以自己设计一个协议.如发送端写入时先写个整数表示消息长度.接收端读取时查看这个整数
中断连接
shutdown
用在closesocket函数之前.通知接收端"不再发送数据".
这个函数可以从容终止连接.但也不是所有流协议都支持从容关闭.如ATM
int shutdown(SOCKET s, int how);
其中how参数可以是:
SD_RECEIVE : 不许再调用接收函数
SD_SEND : 不许再调用发送函数.
SD_BOTH : 两者都取消.
closesocket
关闭套接字.
int closesocket(SOCKET s);
函数执行后.会释放套接字描述符.所以之后对s的调用会失败(产生WSAENOTSOCK)错误.
如果没有对s的其他引用.那所有与s关联的资源都会被释放.包括丢弃所有队列中的数据.
1.7 无连接通信
无连接通信通过UDP完成.
1.7.1 接收端
无连接的套接字接收端(服务器)的创建也是用socket(或WSASocket)函数. 绑定也是用bind函数.
但不必调用listen和accept函数. 只要直接等待着接收数据就行了.
接收函数recvfrom 如下:
int recvfrom( SOCKET s, char FAR* buf, int len, int flags,
struct sockaddr FAR* from, int FAR* fromlen);
其中前4个参数和recv的意义一样. from参数是指向SOCKADDR结构的指针.表示发送端的IP和端口.
from和fromlen参数指向的结构体会在函数执行时被设置.
在winsock2版本里的接收函数WSARecvFrom:
int WSARecvFrom(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDOWRD lpFlags,
struct sockaddr FAR* lpFrom, LPINT lpFromlen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpComletionRoutine);
用UDP接收数据的一个例子
#include <winsock2.h>
void main() {
WSADATA wsaData;
SOCKET ReceivingSocket;
SOCKADDR_IN ReceiverAddr;
int Port = 5150;
char ReceiveBuf[1024];
int BufLength = 1024;
SOCKADDR_IN SenderAddr;
int SenderAddrSize = sizeof(senderAddr);
WSAStartup(MAKEWORD(2,2), &wsaData);
ReceivingSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
ReceiverAddr.sin_family = AF_INET;
ReceiverAddr.sin_port = htons(Port);
ReceiverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(ReceivingSocket, (SOCKADDR*)&SenderAddr, sizeof(SenderAddr));
//接收数据
recvfrom(ReceivingSocket, ReceiveBuf, BufLength, 0,
(SOCKADDR*)&SenderAddr, &SenderAddrSize);
closesocket(ReceivingSocket);
WSACleanup();
}
1.7.2 发送端
在无连接的套接字上发送数据.一种办法是建立套接字后调用sendto或WSASendTo.如下:
int sendto(SOCKET s, const char FAR* buf, int len, int flags,
const struct sockaddr FAR* to, int tolen);
参数分别是: 套接字.缓冲区.要发送的数据字节数.标志.目标地址的指针.目标地址结构的长度.
对应的winsock2中的函数是:
int WSASendTo(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, DWORD dwFlags,
const struct sockaddr FAR* lpTo, int iToLen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
用UDP发送数据的一个例子:
#include <winsock2.h>
void main(){
WSADATA wsaData;
SOCKET SendingSocket;
SOCKADDR_IN ReceiverAddr;
int port = 5150;
char SendBuf[1024];
int BufLength = 1024;
WSAStartup(MAKEWORD(2,2), &wsaData);
SendingSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
ReceiverAddr.sin_family = AF_INET;
ReceiverAddr.sin_port = htons(Port);
ReceiverAddr.sin_addr.s_addr = inet_addr("136.149.3.29");
//发送数据
sendto(SendingSocket, SendBuf, BufLength, 0,
(SOCKADDR*) &ReceiverAddr, sizeof(RecieverAddr));
closesocket(SendingSocket);
WSACleanup();
}
1.8 其他API函数
getpeername
用于获取通信方的套接字地址信息.
int getpeername(SOCKET s, struct sockaddr FAR* name, int FAR* namelen);
getsockname
获取给定套接字的本地接口的地址信息.
int getsockname(SOCKET s, struct sockaddr FAR* name, int FAR* namelen);
WSADuplicateSocket
用来建立WSAPROTOCOL_INFO结构.该结构可以传递到另一个进程.这样就可以用另一个进程打开指向同一个
下层套接字的句柄来对该资源操作.
int WSADuplicateSocket(SOCKET s, DWORD dwProcessId,
LPWSAPROTOCOL_INFO lpProtocolInfo);
其中
s是要复制的套接字句柄.
dwProcessId是另一个进程的ID.
lpProtocolInfo是指向WSAPROTOCOL_INFO结构的指针.它包含目标进程打开s时所需要的信息.

