一、初使化Winsock
如果没有初使化的话,所有的Winsock函数操作都回失败(反回SOCKET_ERROR),错误代码为WSANOTINITIALISED。
初使化函数:
int WSAStartup(WORD 版本号,LPWSADATA pWSADATA)
版本号的建立可以用用宏:MAKEWORD(x,y)
WSADATA结构:
{
WORD 版本
WORD 高版本
char[] 描述
char[] 系统状态
unsigned short iMaxSockets(兼容低版本保留)
unsigned short iMaxUdpDg(兼容低版本保留)
char Far* lpVendorInfo也是兼容保留
}
这是我机子上连结后的运行情况
WSAStartup(MAKEWORD(2,2),&wsaData);
wVersion |
514 |
unsigned short |
wHighVersion |
514 |
unsigned short
|
szDescription |
0x0012fd18 "WinSock 2.0" |
char [257] |
szSystemStatus |
0x0012fe19 "Running" |
char [129] |
iMaxSockets |
0 |
unsigned short |
iMaxUdpDg |
0 |
unsigned short |
lpVendorInfo |
0xcccccccc <错误的指针> |
char * |
514就是0x202,也是我们的版本号。最后三项被忽略了
下面一张表是各个平台的支持的winsock版本
Platform |
Winsock Version |
Windows 95 |
1.1 (2.2) |
Windows 98 |
2.2 |
Windows Me |
2.2 |
Windows NT 4.0 |
2.2 |
Windows 2000 |
2.2 |
Windows XP |
2.2 |
Windows CE |
1.1 |
int WSACleanup():
终止使用Winsock函数。
二、错误信息
使用当Winsock函数返回SOCKET_ERROR时用int WSAGetLastError(void)检测错误代码。错误的代码所对应的错误名称可以在winsock.h或winsock2.h里找到。
h_errno为该指定的宏。
三、选择一个协议
这里简单讲讲通过Internet Protocol(IP)协议建立最基本的Winsock。之所以现在有很大一部分的winsock程序都用它,最主要的原因是它具有广泛的通用性。winsock还可以用别的协议,比如IPX之类的。
从设计上讲,IP是连接协议但不是数据传输协议。我们可以用Two higher-level protocols-Transmision Control Protocol(TCP)或者是User Datagram Protocol(UDP),他们都是通过IP,我们一起讲就是TCP/IP,UDP/IP。如果你要用IPv4(IP version 4),那你必须要要知道怎样使用IPv4
使用IPv4
在IPv4里面,计算机的分配的一个地址是32位,当客户端想通过TCP或者UDP连接,那必须要知道主机的IP地址和端口。同样,主机要监听客户端的请求,那必须要表明一个IP地址和端口。在Winsock里面,程序表明IP地址和服务端口信息是通过SOCKADDR_IN结构。他的声明如下:
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
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;
sin_family必须填AF_INET告诉Winsock我们用的是IP地址
sin_port说明我们选择哪个TCP或者UDP的端口作为我们的通讯端口,对了,有些端口号保留给一些服务,比如说FTP,HTTP等
sin_addr存储IPV4地址用4个字节,就像无符号长整型(DWORD),IP地址在互联网上一般用形如a.b.c.d格式。
sin_zero只不过是让SOCKADDR_IN和SOCKADDR结构大小一样。
下面是一个很有用的函数,把a.b.c.d格式的IP地址转成无符号长整型。
unsigned long inet_addr(const char FAR* cp);
字节顺序
不同的计算机处理数字有两种形式,big-endian和little-endian型式(
little-endian格式的数据,例如0X12345678以(0X78 0X56 0X34 0X12)方式保存、
big-endian格式的数据,例如0X12345678以(0X12 0X34 0X56 0X78)方式保存 ),这依赖于他们是怎么设计的,比如Intel的x86处理器,多字节是用little-endian型式。IP地址和和端口在电脑中是多字节存放的,他们是host-byte顺序,然而当IP地址和端口通过网络时,必须转成big-endian形式,也就是network-byte顺序
有一系列函数完成两者之间的转换。比如
host-byte序转network-byte序
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 );
network-byte序转host-byte序
u_long ntohl(u_long netlong);
int WSANtohl( SOCKET s, u_long netlong, u_long FAR * lphostlong );
u_short ntohs(u_short netshort);
int WSANtohs( SOCKET s, u_short netshort, u_short FAR * lphostshort );
例:
SOCKADDR_IN addr;
INT port = 8080;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("216.239.57.99");
addr.sin_port = htons(port);
四、建立socket
通过API SOCKET socket(int af,int type,int protocol);
第一个参数是协议的地址类别,比如我们前面讲用的是IPv4,那么af就是AF_INET,
第二个参数是协议的socket类型,你用TCP/IP时,type=SOCK_STREAM,你用UDP/IP时,type=SOCK_DGRAM,
第三个参数是协议是(未详),如果是TCP的话,则该处是IPPROTO_TCP,如果是UDP的话,则该处是IPPROTO_UDP
五、服务器API函数
服务器是一个进程,用来等待不定数目的客户端连接,响应客户端的请求。一个服务器必须有一个可供客户端定位的名字,在TCP/IP里,这个名字是IP地址和端口。
第一步:用socket或者WSASocket建立socket,并用bind绑定
第二步:socket进入监听模式。(listen)
最后:当客户端发出请求时响应请求。(accept或者WSAAccept)
绑定(Binding)
int bind( SOCKET s, const struct sockaddr FAR* name, int namelen );
第一个参数是要绑定的socket;
第二个参数是表明你在使用的协议
第三个参数表明你指定协议地址结构的长度。
例:
SOCKET s;
SOCKADDR_IN addr;
int port = 5555;
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(s, (SOCKADDR *)&addr, sizeof(addr));
监听(Listening)
把socket转成监听模式。bind只不过是绑定,listen是告知socket进入等待进入的连接。
int listen( SOCKET s, int backlog );
第一个参数是绑定过的socket
第二个参数是最大队列长度,比如说这个数设为二,与此同时有三个客户连入,那么先进来的二个进入队列,第三个则会收到WSAECONNREFUSED错误信息。注意服务器Accept了一个连接,这个连接就会从队列中移除。
如果你没bind而直接listen的话会收到 WSAEINVAL 出错信息。
同意连接(Accepting Connectino)
SOCKET accept( SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen );
第二个参数是你收到的客户端地址。
第三个参数addrlen 表明addr的长度
六、客户端API函数
第一步:建立socket
第二步:设置你要连接对象的SOCKADDR地址
第三步:用connect 或者WSAConnect连接。
TCP状态
起初每个socket都是CLOSED状态,当客户端初使化一个连接,他发送一个SYN包到服务器,客户端进入SYN_SENT状态。 服务器接收到SYN包,反馈一个SYN-ACK包,客户端接收后返馈一个ACK包客户端变成ESTABLISHED状态,如果长时间没收到SYN-ACK包,客户端超时进入CLOSED状态。 当服务器绑定并监听某一端口时,socket的状态是LISTEN,当客户企图建立连接时,服务器收到一个SYN包,并反馈SYN-ACK包。服务器状态变成SYN_RCVD,当客户端发送一个ACK包时,服务器socket变成ESTABLISHED状态。
当一个程序在ESTABLISHED状态时有两种图径关闭它, 第一是主动关闭,第二是被动关闭。如果你要主动关闭的话,发送一个FIN包。当你的程序closesocket或者shutdown(标记),你的程序发送一个FIN包到peer,你的socket变成FIN_WAIT_1状态。peer反馈一个ACK包,你的socket进入FIN_WAIT_2状态。如果peer也在关闭连接,那么它将发送一个FIN包到你的电脑,你反馈一个ACK包,并转成TIME_WAIT状态。 TIME_WAIT状态又号2MSL等待状态。MSL意思是最大段生命周期(Maximum Segment Lifetime)表明一个包存在于网络上到被丢弃之间的时间。每个IP包有一个TTL(time_to_live),当它减到0时则包被丢弃。每个路由器使TTL减一并且传送该包。当一个程序进入TIME_WAIT状态时,他有2个MSL的时间,这个充许TCP重发最后的ACK,万一最后的ACK丢失了,使得FIN被重新传输。在2MSL等待状态完成后,socket进入CLOSED状态。 被动关闭:当程序收到一个FIN包从peer,并反馈一个ACK包,于是程序的socket转入CLOSE_WAIT状态。因为peer已经关闭了,所以不能发任何消息了。但程序还可以。要关闭连接,程序自已发送给自已FIN,使程序的TCP socket状态变成LAST_ACK状态,当程序从peer收到ACK包时,程序进入CLOSED状态。
|
connect
int connect( SOCKET s, const struct sockaddr FAR* name, int namelen );
第二个参数是你要连接的名字
第三个参数是你加接的名字参数的长度
如果连接失败了则返馈WSAECONNREFUSED错误。
send和WSASend
int send( SOCKET s, const char FAR * buf, int len, int flags );
第二个参数是要发送的数据。
第三个参数是发送数据的长度。
第四个参数可以是0,MSG_DONTROUTE或者是MSG_OOB,这几个参数之间能用or连接。
正常返回:发送的字节。
int WSASend(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
最后两个参数用于重叠I/O,重叠I/O是一个种异步I/O模型。
WSASendDisconnect
int WSASendDisconnect ( SOCKET s, LPWSABUF lpOutboundDisconnectData );