Windows网络编程(基础篇1)
- Winsock是一种网络编程接口,不是协议。
- 除了WSAStartup、WSACleanup、WSARecvEx、WSAGetLastError属于Winsocket1.1规范函数外,凡是有前缀WSA的,都是在Winsock 2 中更新或者增添的一个新的API函数。
一、Winsock初始化
包含头文件winsock2.h,链接库WS2_32
include <winsock2.h> #pragma comment(lib,"WS2_32")
使用Winsock的应用都必须加载合适的Winsock DLL版本,否则返回SOCKET_ERROR。使用WSAStartup加载,最后需要调用WSACleanup释放Winsock分配的资源。
int WSAStartup( _In_ WORD wVersionRequested, _Out_ LPWSADATA lpWSAData );
- wVersionRequested:版本号,高阶字节指定小版本号,低位字节指定主版本。
- lpWSAData 指向WSADATA数据结构的,接收Windows Sockets实现细节。
WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData);//成功返回0
MAKEWORD:创建一个无符号16位整形,通过连接两个给定的无符号参数,也就是将(2,2)放入wVersionRequested中。
WSAGetLastError();//返回调用winsock函数发生的错误代码 WSACleanup();//程序结束时,需要调用释放资源
二、SOCKADDR_IN简介
SOCKADDR_IN:用来指定IP地址和端口信息。
typedef struct sockaddr_in { short sin_family; //The address family for the transport address,must AF_INET USHORT sin_port; //port number IN_ADDR sin_addr; // IPv4 transport address CHAR sin_zero[8]; //Reserved(预留) for system use } SOCKADDR_IN, *PSOCKADDR_IN;
inet_pton 转换字符串到网络地址。将“点分十进制” -> “二进制整数”(inet_addr已弃用)
//m_HostGroup.sin_addr.s_addr = inet_addr(strGroupIP);//代替方法如下:
inet_pton(AF_INET, strGroupIP, (void*)&m_HostGroup.sin_addr.s_addr);
INT WSAAPI InetPton( _In_ INT Family, // AF_INET and AF_INET6. _In_ PCTSTR pszAddrString, //待转换的地址,IPV4 或 IPV6 _Out_ PVOID pAddrBuf //转换后的(IPV4:IN_ADDR,IPV6: IN6_ADDR );
htons 将整型变量从主机字节顺序转变成网络字节顺序
u_short WSAAPI htons(_In_ u_short hostshort);
ntohl 将网络字节顺序转换成主机字节顺序
u_long WSAAPI ntohl(_In_ u_long netlong);
- 创建SOCKADDR_IN结构示例:
SOCKADDR_IN sin; WORD Port=80; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(Port); inet_pton(AF_INET, "192.168.1.1", (void*)&sin.sin_addr.S_un.S_addr);
三、套接字通信(TCP)
创建套接字函数:socket、WSASocket;
SOCKET WSAAPI socket( _In_ int af, //指定协议族 AF_INET、AF_INET6、AF_LOCAL等 _In_ int type, //指定Socket类型 (TCP)SOCK_STREAM、(UDP)SOCK_DGRAM、SOCK_RAW等 _In_ int protocol //指定协议 IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP等 );
创建套接字后,就必须将套接字绑定到一个已知地址上:bind;
int bind( _In_ SOCKET s, //待连接的套接字 _In_ const struct sockaddr *name, //地址缓冲区(sockaddr *)sockaddr_in _In_ int namelen //name大小 );
将套接字置入监听模式:listen;(bind只是将套接字和指定地址关联,listen指示套接字等候连接)
int listen( _In_ SOCKET s, //待监听的套接字 _In_ int backlog //等待连接队列的最大长度 );
有客户端连接到达时,接收一个连接:accept ;
SOCKET accept( _In_ SOCKET s, //正在监听的套接字 _Out_ struct sockaddr *addr, //连接者的地址 _Inout_ int *addrlen //指向存有addr地址长度的整数 );
客户端通过套接字连接到服务端:connect;
int connect( _In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen );
服务端示例:
#include<winsock2.h> #pragma comment(lib,"WS2_32") int main(void) { WSADATA wsaData; if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { return 0; } SOCKET ListeningSocket; SOCKET NewConnetction; SOCKADDR_IN ServerAddr; SOCKADDR_IN ClientAddr; int Port = 5150; //创建一个套接字来监听客户端连接 ListeningSocket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(Port); ServerAddr.sin_addr.S_un.S_addr = INADDR_ANY; //用bind将套接字信息和地址信息绑定 ::bind(ListeningSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr)); //监听客户连接,限制5个 ::listen(ListeningSocket, 5); //连接到达时,接受一个新连接 int ClientaddrLen; NewConnetction = ::accept(ListeningSocket, (SOCKADDR*)&ClientAddr, &ClientaddrLen); //此时在这些套接字上可以做: //1.在ListeningSocket上再次调用accept,等待更多的连接。2.在NewConnection上完成数据收发。 //关闭套接字 closesocket(NewConnetction); closesocket(ListeningSocket); WSACleanup(); return 1; }
客户端示例:
#include<winsock2.h> #include<Ws2tcpip.h> #pragma comment(lib,"WS2_32") int main(void) { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { return 0; } SOCKET S; SOCKADDR_IN ServerAddr; int Port = 5150; //创建一个套接字来建立客户端连接 S = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(Port); inet_pton(AF_INET, "192.168.1.1", (void*)&ServerAddr.sin_addr.S_un.S_addr); connect(S, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr)); //关闭套接字 closesocket(S); WSACleanup(); return 1; }
为什么客户端不用bind
- 无连接的socket的客户端和服务端以及面向连接socket的服务端通过调用bind函数来配置本地信息。使用bind函数时,通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。
- 有连接的socket客户端通过调用Connect函数在socket数据结构中保存本地和远端信息,无须调用bind(),因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,socket执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候打开端口。
- 服务端进程bind IP地址:目的是限制了服务端进程创建的socket只接受那些目的地为此IP地址的客户链接
1.需要在建连前就知道端口的话,需要 bind
2.需要通过指定的端口来通讯的话,需要 bind
四:数据传输
要在已经建立的套接字上发送数据,可以用send和WSASend。接收数据可以用recv和WSARecv。
int send( _In_ SOCKET s, //是一个已经建立了连接,用于发送数据的套接字 _In_ const char *buf, //指向即将发送数据的缓冲区 _In_ int len, //缓冲区内的字符数 _In_ int flags //调用执行方式 );//如果成功,返回的是发送的字节数,否则返回SOCKET_ERROR
int WSASend( _In_ SOCKET s, _In_ LPWSABUF lpBuffers, _In_ DWORD dwBufferCount, _Out_ LPDWORD lpNumberOfBytesSent, _In_ DWORD dwFlags, _In_ LPWSAOVERLAPPED lpOverlapped, _In_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
char sendbuff[2048];int nBytes=2048; sendbuff=******;
ret=send(s,sendbuff,nBytes,0);
对于send函数而言,可能返回已发出的字节数少于给定的字节数。因为对于每个收发数据的套接字来说,系统都为他们分配了相当充足的缓冲区空间,所以返回值ret变量将被设为已发送的字节数。在发送数据时,内部缓冲区都会将数据一致保留到可以将它发到线上为之。比如,传输大量的数据可以领缓冲区快速填满。同时,对TCP/IP来说,还有一个窗口大小的问题。接收端会对窗口大小进行调节,以指示它可以接收多少数据。如果有大量数据涌入接收端,接收端就会将窗口大小设为0,为挂起数据做好准备。对发送端来说,这样会强制它在收到一个新的大于0的窗口大小之前,不得再发送数据。在使用send调用时,缓冲区可能只能容纳1024字节,这时,便有必要重新提交剩下的1024字节。
char sendbuff[2018]; int nBytes=2048, nLeft, idx; nLeft=nBytes; idx=0; while(nLeft>0) { ret=send(s,&sendbuff[idx],nLeft,0); if(ret==SOCKET_ERROR) { //error } nLeft-=ret; idx+=ret; }
在已经建立连接的套接字上接受数据的传入,可以使用recv和WSARecv。
int recv( _In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags );
int WSARecv( _In_ SOCKET s, _Inout_ LPWSABUF lpBuffers, _In_ DWORD dwBufferCount, _Out_ LPDWORD lpNumberOfBytesRecvd, _Inout_ LPDWORD lpFlags, _In_ LPWSAOVERLAPPED lpOverlapped, _In_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
流套接字是一个不间断的数据流,在读取它时,应用程序通常不会关心应该读多少数据。如果所有消息长度都一样,则处理要简单,比如读取512字节。
char recvbuff[1024]; int ret, nLeft, idx; nLeft=512; idx=0; while(nLeft>0) { ret=recv(s,&recvbuff[idx],nleft,0); if(ret==SOCKET_ERROR) { //error } idx+=ret; nLeft-=ret; }
如果消息长度不同,就必须要利用自己的协议来通知接收端,让它知道即将到来的消息长度是多少。比如,写入接收端的前4个字节总是整数,用来标记即将到来的消息长度。
中断连接
一旦完成了套接字连接,就必须将它关掉,并释放关联到那个套接字句柄的所有资源,执行closesocket即可。但是,closesocket可能带来的负面影响就是导致数据丢失。鉴于此,在调用closesocket函数之前,利用shutdown函数从容中止连接。
int shutdown( _In_ SOCKET s, _In_ int how //SD_RECEIVE,SD_SEND, SD_BOTH );//关闭一个套接字
对closesocket调用释放的套接字描述符,如果再次利用该套接字就会调用失败。如果没有对改套接字的其他引用,那么所有与套接字描述符相关的资源都会被释放,包括丢弃所有队列中的数据。