1.winsock 的初始化
int WASStartup(WORD wVersionRequested, LPWSADATA lpWSADATA);
wVersionRequested 高字节制定所需winsock库的次版本,低字节是主版本。可以使用宏MAKEWORD(x, y), x为高字节,y为低字节。
typdef strcut WSADATA
{
WORD wVersion; // 将要使用的winsock版本
WORD wHightVersion; //包含现有winsock库的最高版本
char szDescription[WSADESCRIPTION_LEN + 1];
char szSystemStatus[WDASYS_STATUS_LEN + 1];
unsigned short iMaxSocket; //同时打开的最大套接字数量
unsigned shorot iMaxUdpDg; // 数据表的最大长度
char FAR* lpVendorInfo;
}
在使用winsock接口编写好应用程序之后,应该使用WSACleanup函数,这个函数能够使得winsock释放所有由winsock分配的资源,并取消这个应用程序挂起的winsock调用。
int WSACleanup(void);
2. 错误检查和处理
int WSAGetLastError(void);
返回winsock的错误代码。
3. 协议寻址
3.1 IPV4寻址
ipv4中,计算机分配一个32位的地址,客户机要通过tcp或udp和服务器通信时,必须指定一个ip地址和一个端口号,服务器打算监听传入的客户端请求时,也必须指定一个ip地址和端口号。
在winsock中,应用程序通过SOCKADDR_IN结构指定ip地址和服务器端口号。
struct sockaddr_in
{
short sin_family; //字段必须为AF_INT, 以告知winsock此时正在使用ip地址簇。
u_short sin_port; //标识服务器服务的TCP或UDP通信端口。
struct in_addr sin_addr; //把ip地址作为一个4字节存储起来。
char sin_zero[8];//字节填充,使得SOCKADDR_IN和SOCKADDR结构的长度一样。
};
网络字节顺序采用big-endian(从最有意义的字节到最无意义的字节,intel86处理器采用little-endian)。
主机字节转换为网络字节顺序的四个API:
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);
网络字节顺序转换为主机字节顺序的四个API:
u_long ntohl(u_long netlong);
int WSANtohl(SOCKET s, u_long netlong, ulong FAR* lphostlong);
u_short ntohs(u_short netshort);
int WSANtohs(SOCKET s, u_short netshort, u_short FAR* lphostshort);
4. 创建套接字
两个函数socket和WSASocket可以创建套接字,socket定义如下:
SOCKET socket(
int af, // ipv4应设为AF_INET
int type, //协议的套接字类型 TCP/IP创建套接字,设为SOCK_STREAM, UDP/IP应设为SOCK_DGRAM
int protocol //用于给定地址族和套接字类型具有多重入口时,对具体的传送做限定,TCP应设为IPPROTO_TCP, UPD应设为IPPPROTO_UDP
);
5. 面向连接的通信
在IP中,面向连接的通信通过TCP/IP完成的,TCP提供两个计算机之间可靠无误的数据传输,TCP通信在源计算机和目标计算机之间建立一个虚拟连接,建立连接后,计算机之间便能以双向字节流的防暑进行数据交换。
5.1服务器API函数
服务器等待任意数量的客户机与之建立连接,必须在一个已知的名称上监听连接。在TCP/IP中,这个名称就是本地IP地址,再加上一个端口号。
winsock第一步是通过bind API将一个给定的套接字绑定到已知的名称上,第二步用listen API函数将套接字设为监听模式,最后,若客户端试图建立连接,服务器必须通过accept或WSAAccept调用接受连接。
5.1.1绑定
bind函数可将指定的套接字同一个已知地址绑定到一起,函数如下:
int bind(
SOCKET s, //等待客户端连接的套接字
const struct sockaddr FAR* name, //根据使用的协议,填充一个地址缓冲区
int namelen //name缓冲区的地址长度
);
5.1.2监听
指示套接字等候连接传入的api是listen函数,定义如下:
int listen(
SOCKET s, //被绑定的套接字
int backlog //被搁置的连接最大长度
);
5.1.3接受连接
通过函数accept, WSAAccept, AcceptEx函数完成。accept函数如下:
SOCKET accept(
SOCKET s, // 绑定的套接字,处于监听模式
struct sockaddr FAR* addr, //协议对应的SOCKADDR地址,函数返回后,这个结构会包含发出连接请求的客户端的ip地址信息,对于ipv4,返回ipv4的信息,对应的结构体为SOCKADDR_IN.
int FAR* addrlen //返回addr的结构的长度
);
函数返回一个新的套接字描述符,对应已经接受的那个客户端连接。该客户端的所有后续操作都使用这个套接字。
5.2客户端API函数
客户端创建要简单的多,只许3个步骤
1.创建一个套接字。
2.建立一个SOCKADDR地址结构,结构名称为准备连接到的服务器名。对于TCP/IP, 为客户端所监听的服务器的IP地址和端口号。
3.用connect或WSAConnect初始化客户端与服务器的连接。
connect函数定义如下:
int connect(
SOCKET s, //建立连接的有效TCP套接字。
const struct sockaddr FAR* name, //TCP套接字的地址结构,(SOCKADDR_IN),表示要连接的服务器。
int namelen //name参数的长度
);
5.3数据传输
要在已建立连接的套接字上发送数据,用send和WSASend,在已建立连接的套接字上接受数据recv和WSARecv。
所有收发数据的缓冲区都属于简单的char类型,即面向字节。
5.3.1 send和WSASend
int send(
SOCKET s, //已建立连接,用于发送数据的套接字
const char FAR* buf, //即将发送的数据
int len, //buf大小
int flags // 可为0,MSG_DONTROUTE或MSG_OOB,MSG_DONTROUTE。要求传输层不要将发出的数据路由出去,是否由下一层的传输来决定。
);
成功的情况下,send返回发送的字节数,错误返回SOCKET_ERROR。
int WSASend(
SOCKET s, //连接会话的有效句柄
LPWSABUF lpBuffers, //指向一组WSABUF结构的指针
DWORD dwBufferCount, //WSABUF的数量
LPDWORD lpNumberOfBytesSent, //写入包含已发送的字节数
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped, //重叠IO
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //重叠IO完成例程
);
如果成功,返回0,否则返回SOCKET_ERROR
5.3.2 WSASendDisconnect
这个函数非常特殊,一般不用,原型如下:
int WSASendDisconnect(
SOCKET s,
LPWSABUF lpOutboundDisconnectData
);
5.3.3 recv和WSARecv
int recv(
SOCKET s, //准备接受数据的套接字
char FAR* buf, //接受数据的缓冲区
int len, //数据缓冲区的长度
int flags //
);
int WSARecv(
SOCKET s, //连接会话的有效句柄
LPWSABUF lpBuffers, //指向一组WSABUF结构的指针
DWORD dwBufferCount, //WSABUF的数量
LPDWORD lpNumberOfBytesSent, //收到的字节数
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped, //重叠IO
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //重叠IO完成例程
);
5.3.4WSARecvDisconnect
int WSARecvDisconnect(
SOCKET s,
LPWSABUF lpInboundDisconnectData
);
和WADSendDisconnect一样,接收到的数据时断开数据,时另一端的WSASendDisconnect调用发出的,不能用于接受普通数据。
5.4流协议
流协议中,发送者和接受者可以将数据分解成小数据块,或者将数据合并为大数据块。
对于流套接字上收发数据所用的函数,它们不能保证需要读取或写入的数据的数量。
散播-聚集IO:比如客户端发送到服务器的消息可能这样构成的,一个指定某种操作固定的32字节的头,一个64字节的数据块和一个16字节的尾。
就可以用3个WSABUF结构组成的数字调用WSASend,这3个结构分别对应3种消息类型,在接收端,3个WSABUF调用WSARecv。
5.5中断连接
完成了套接字连接,就要关掉它,并释放关联到套接字的所有资源,真正的释放与套接字关联的资源,调用closesocket即可。
但closesocket可能会带来负面影响,可能会导致数据的丢失。因此在调用closesocket之前,利用shutdown从容的终止连接。
5.5.1shutdown
为了保证通信双方能够收到应用程序发出的所有数据,对于一个好的应用程序来说,应该通知接收端“不再发送数据”。同样,通信对方也应如此。
int shutdown(
SOCKEt s,
int how
);
how 参数可以是后面的任何一个值:SD_RECEIVE, SD_SEND, SD_BOTH。
SD_RECEIVE表示不允许再调用接收函数。
SD_SEND表示不允许再调用发送函数。
SD_BOTH表示取消两端的收发操作。
并非所有的面向连接协议都支持从容关闭。
5.5.2 closesocket
int closesocket(SOCKET s);
该函数会释放套接字描述符,在利用套接字的调用就会失败,出现WSAENOTSOCK,如果没有对套接字的其他引用,那么与套接字关联的资源都被释放,
包括丢弃队列中的数据。
6.无连接通信
在IP中,无连接通信通过UDP/IP完成,UDP不能确保可靠的数据传输,但能将数据发送到多个目标,或者接受多个源数据。
6.1接收端
先用socket或WSASocket创建套接字,再把这个套接字和准备接受数据的借口绑定在一起,通过bind函数完成,但不调用listen和accept函数。
只等待接受数据。
int recvfrom(
SOCKET s,
char FAR* buf,
int len,
int flags,
struct sockaddr FAR* from, //填入发送数据的工作站地址
int FAR* fromlen
);
返回读取的字节总数。
Winsock2版本的WSARecvFrom
int WSARecvFrom(
SOCKET S,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfByteRecvd,
LPDWORD lpFlags,
struct sockadd FAR* lpFrom,
LPINT lpFromlen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
无连接套接字上接受(发送)数据的另一种方法是建立连接,无连接的套接字一旦建立,便可以利用SOCKADDR参数(被设为与之通信的远程接收端的地址)
调用connect或者WSAConnect。但事实上没有建立连接。
6.2发送端
先建立个套接字,然后调用sendto或者WSASendto
int sendto(
SOCKET s,
const char FAR* buf,
int len,
int flags,
const struct sockaddr FAR* to, //接收数据的工作站的目标地址
int tolen
);
int WSASendto(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfByteSent, //真正发送到接收端的字节数
DWORD dwFlags,
const struct sockaddr FAR* lpTo, //接收端的地址信息
int iTolen, //sockaddr的结构长度
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpOverlappedRoutine
);
6.3基于消息的协议
面向连接的通信同时也是流协议,无连接通信几乎都是基于消息的。
首先,由于面向消息的协议保留了数据边界,所以提交给发送函数的数据在被发送完之前会形成阻塞,对非阻塞IO模式而言,如果数据未能完全发送,
发送函数就会返回WSAEWOLUDBLOCK,这意味着下层的系统不能对不完整的数据进行处理,应该稍后再次调用发送函数。
在连接的另一端,对接受函数必须提供一个足够大的缓冲区,如果提供的缓冲区不够大,接收调用就会失败,将出现WSAEMSGSIZE错误。
发生这种情况时,缓冲区就会被填满,但未完全收完的数据会被抛弃。被截断的数据也无法恢复。
唯一的例外是支持部分消息的协议,当接受调用仅收到部分消息时,在WSARecv,WSARecvFrom返回前,将flag设为MSG_PARTIAL。
6.4释放套接字资源
由于无连接协议没有连接,所以也不会有对连接的正式关闭和从容关闭,在接收端或发送端完成接收发送数据时,只需调用closesocket。
7. 其他API函数
7.1 getpeername
获得连接上来的客户端的套接字地址信息,该信息是关于已建立的那个套接字的。
int getpeername(
SOCKET s,//连接上来的套接字
struct sockaddr FAR* name, //返回的是传递到连接调用的地址
int FAR* namelen
);
7.2 getsockname
返回的是给定套接字的本地接口的地址信息,就是本地sock的信息
int getsockname(
SOCKET s,
struct sockaddr FAR* name,
int FAR* namelen
);
7.3WSADuplicateSocket
可以用另一个进程打开指向同一个下层套接字句柄,另外的进程也能对该套接字进行操作。必须考虑进程间通信。
int WSADuplicateSocket(
SOCKET s, //准备复制的套接字句柄
DWORD dwProcessId, //打算使用所复制的套接字的进程ID。
LPWSAPROTOCOL_INFO lpProtocolInfo //指向目标进程打开复制句柄所需的信息。
);
此函数复制的是套接字的描述符,而不是实际套接字,如果一个进程在复制的套接字上调用closesocket,会导致进程中的描述符被释放;
但在最后留下的那个描述符上调用closesocket之前,下层套接字会保持打开状态。