转自: http://blog.csdn.net/zhao_yin/article/details/6982382
一、基本知识
1、Winsock,一种标准API,一种网络编程接口,用于两个或多个应用程序(或进程)之间通过网络进行数据通信。具有两个版本:
Winsock 1:
Windows CE平台支持。
头文件:WinSock.h
库:wsock32.lib
Winsock 2:
部分平台如Windows CE貌似不支持。通过前缀WSA可以区别于Winsock 1版本。个别函数如WSAStartup、WSACleanup、WSARecvEx、WSAGetLastError都属于Winsock 1.1规范的函数;
头文件:WinSock2.h
库:ws2_32.lib
mswsock.h用于编程扩展,使用时必须链接mswsock.dll。
2、网络协议:
IP (Internet Protocol) 网际协议,无连接协议;
TCP (Transmission Control Protocol) 传输控制协议;
UDP (User Datagram Protocol) 用户数据协议;
FTP (File Transfer Protocol) 文件传输协议;
HTTP (Hypertext Transfer Protocol) 超文本传输协议;
3、字节存储顺序:
big_endian:大端存储,存储顺序从高位到低位,地址指向最高有效字节。在网络中将IP和端口指定为多字节时使用大端存储,也称为网络字节顺序(network_byte)。貌似MAC OS使用的是大端存储方式;
little_endian:小端存储,存储顺序从低位到高位,地址指向最低有效字节。本地主机存储IP和端口制定的多字节时使用,也称为主机字节顺序(host_byte)。大多数系统都是小端存储;
用下面的方式可以检测是否为大端存储:
- bool IsBig_endian()
- {
- unsigned short test = 0x1122;
- if ( *( (unsigned char*)&test ) == 0x11 )
- {
- return true;
- }
- else
- {
- return false;
- }
- }
此外有很多函数可以用来进行 主机字节和网络字节之间的转换,如:
u_long htonl( u_long hostlong );
int WSAHtonl( SOCKET s, u_long hostlong, u_long FAR *lpnetlong );
而有时网络IP是点分法表示的,如:192.168.0.1,使用函数 unsigned long inet_addr( const char FAR *cp ); 可以将点分法的IP字符串作为一个网络字节顺序的32位u_long返回。
二、快速了解
1、Winsock初始化:
首先确保包含对应版本的头文件,然后保证链接对应的库文件(可以在代码中使用#pragma comment(lib, "WS2_32"),或在编译器项目属性中链接器->输入->附加依赖项中添加ws2_32.lib);
通过调用WSAStartup函数来实现加载Winsock库:
- int
- WSAAPI
- WSAStartup(
- IN WORD wVersionRequested,
- OUT LPWSADATA lpWSAData
- );
其中参数wVersionRequested用来指定加载Winsock库的版本,高位字节为次版本,低位字节为主版本,使用宏 MAKEWORD(x,y)来生成一个WORD;
参数lpWSAData是指向WASDATA结构指针,加载的版本库信息将会填充这个结构,详细内容自查。
在使用Winsock后需要释放资源,并取消应用程序挂起的Winsock操作。使用 int WASCleanup();
2、错误处理:
如果已经加载了Winsock库,则调用Winsock函数出错后,通常会返回SOCKET_ERROR,而通过使用函数 int WSAGetLastError() 可以获得具体信息值,例如:
- if ( SOCKET_ERROR == WSACleanup() )
- {
- cout << "WSACleanup error " << WSAGetLastError() << endl;
- return 0;
- }
根据获取错误信息值,可以知道错误原因,并进行相应的处理。
3、寻址:
想要进行通信就需要知道彼此的地址,一般来说这个地址由IP和端口号来决定。在Winsock中使用SOCKADDR_IN结构来指定地址信息:
- struct sockaddr_in {
- short sin_family;
- u_short sin_port;
- struct in_addr sin_addr;
- char sin_zero[8];
- };
sin_family字段通常设置为AF_INET,表示Winsock此时正在使用IP地址族;
sin_port用于标示TCP或UDP通信端口,部分端口是为一些服务保留的,如FTP和HTTP使用要注意;
sin_adr字段把地址(例如是IPv4地址)作为一个4字节的量来存储起来,它是u_long类型,且是网络字节顺序的。可以使用inet_addr来处理点分法表示的IP地址;
sin_zero只充当填充项,以使SOCKADDR_IN结构和SOCKADDR结构长度一样。
以下简单的使用SOCKADDR_IN来指定地址:
- //创建一个地址
- int serverPort = 5150;
- char FAR serverIP[] = "192.168.1.102"; //本机ip,不知道就ipconfig
- SOCKADDR_IN serverAddr;
- serverAddr.sin_family = AF_INET;
- serverAddr.sin_port = htons( serverPort );
- serverAddr.sin_addr.s_addr = inet_addr( serverIP );
- int serverAddr_size = static_cast<int>( sizeof(serverAddr) );
有时作为一个连接通信的服务端来说,在设置监听socket的地址结构时sin_addr.s_addr的值可以是htonl( INADDR_ANY ),INADDR_ANY允许将socket绑定到系统中所有可用的接口,以便传到任意接口的连接(端口必须正确)都可以被监听socket接受。
4、socket套接字:
套接字是通信时为传输提供的句柄,Winsock的操作都是基于套接字实现的。创建一个套接字有socket和WSASocket方法:
- SOCKET
- WSAAPI
- socket(
- IN int af, //协议的地址族,使用IPv4来描述Winsock,设置为AF_INET
- IN int type, //套接字类型,TCP/IP设置为SOCK_STREAM,UDP/IP设置为SOCK_DGRAM
- IN int protocol //用于给定地址族和类型具有多重入口的传送限定,TCP设置为IPPROTO_TCP,UDP设置为IPPROTO_UDP
- );
如果创建成功,函数会返回一个有效的SOCKET,否则会返回INVALID_SOCKET,可以用WSAGetLastError()函数获得错误信息。
5、连接通信实现过程:
连结通信是基于TCP/IP实现的,进行数据传输前,通信双方要进行连接。
服务端:
初始化Winsock后,创建一个监听socket和一个要接受连接的地址结构;
使用bind将监听socket与地址结构进行关联;
- int
- WSAAPI
- bind(
- IN SOCKET s, //一个用于监听的socket
- IN const struct sockaddr FAR * name, //指向进行绑定的地址结构
- IN int namelen //进行绑定的地址结构的大小
- );
使用listen将bind成功的监听socket状态设置为监听状态;
- int
- WSAAPI
- listen(
- IN SOCKET s, //一个用于监听的socket,已经进行bind
- IN int backlog //允许挂起连接的队列的最大长度,超过这个长度后,再有连接将会失败
- );
使用accept接受通过监听socket获取的连接,成功后将返回的新的连接socket进行保存以便数据传输;
- SOCKET
- WSAAPI
- accept(
- IN SOCKET s, //处于监听模式的socket
- OUT struct sockaddr FAR * addr, //指向一个地址结构,用来接受连接后获得对方地址信息
- IN OUT int FAR * addrlen //指向一个整数,表示参数2指向地址结构的大小
- );
客户端:
初始化Winsock后,创建一个监听socket和一个要连接的服务器地址结构;
使用connect将socket和服务器地址结构进行初始化连接,成功后将使用socket进行数据传输;
- int
- WSAAPI
- connect(
- IN SOCKET s, //要建立连接的socket
- IN const struct sockaddr FAR * name, //指向保存要建立连接信息的地址结构
- IN int namelen //参数2指向地址结构的大小
- );
连接成功后,使用send、recv来进行数据传输;
- int
- WSAAPI
- send(
- IN SOCKET s, //进行连接的socket
- IN const char FAR * buf, //指向发送数据的缓冲区
- IN int len, //发送数据的字符数
- IN int flags //一个标志位,可以是0、MSG_DONTROUTE、MSG_OOB还可以是他们的或运算结果
- ); //返回已经发送的数据长度
- int
- WSAAPI
- recv(
- IN SOCKET s, //进行连接的socket
- OUT char FAR * buf, //指向接受数据的缓冲区
- IN int len, //准备接受数据字节数或缓冲区的长度
- IN int flags //可以是0、MSG_PEEK、MSG_OOB还可以是他们的或运算结果
- ); //返回已接受的数据长度
连接结束后,使用shutdown和closesocket来断开连接和释放资源;
- int
- WSAAPI
- shutdown(
- IN SOCKET s, //要关闭的socket
- IN int how //关闭标志:SD_RECEIVE、SD_SEND、SD_BOTH
- );
6、无连接通信实现过程:
无连接通信是基于UDP/IP实现的,UDP不能确保可靠的数据传输,但能将数据发送到多个目标,或者接受多个源的数据。
初始化Winsock后,可以创建socket和用以进行通信任意地址结构;
使用recvfrom通过socket和通信的地址结构接受数据;
使用sendto通过socket和通信的地址结构发送数据;
- int
- WSAAPI
- recvfrom(
- IN SOCKET s,
- OUT char FAR * buf,
- IN int len,
- IN int flags,
- OUT struct sockaddr FAR * from,
- IN OUT int FAR * fromlen
- );
- int
- WSAAPI
- sendto(
- IN SOCKET s,
- IN const char FAR * buf,
- IN int len,
- IN int flags,
- IN const struct sockaddr FAR * to,
- IN int tolen
- );
同样通信结束后,使用shutdown和closesocket来断开连接和释放资源
上述使用函数都有多个版本,而且相关的一些标志位参数可以提供设置选项,另外,返回的错误处理等也有待于详细研究;
7、select函数:
select()用于确定一个或多个套接口的状态。对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息。
- int
- WSAAPI
- select(
- IN int nfds, //指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,在Windows中值无所谓。
- IN OUT fd_set FAR * readfds, //可选指针,指向一组等待可读性检查的套接字。
- IN OUT fd_set FAR * writefds, //可选指针,指向一组等待可写性检查的套接字。
- IN OUT fd_set FAR *exceptfds, //可选指针,指向一组等待错误检查的套接字。
- IN const struct timeval FAR * timeout //select()最多等待时间,对阻塞操作则为NULL。
- );
- //用fd_set结构来表示一组等待检查的套接口。在调用返回时,这个结构存有满足一定条件的套接口组的子集:
- typedef struct fd_set {
- u_int fd_count; //其中set元素数目
- SOCKET fd_array[FD_SETSIZE]; //保存set元素的数组
- } fd_set;
- fd_set set;
- FD_ZERO(&set); /*将set清零使集合中不含任何fd*/
- FD_SET(fd, &set); /*将fd加入set集合*/
- FD_CLR(fd, &set); /*将fd从set集合中清除*/
- FD_ISSET(fd, &set); /*测试fd是否在set集合中*/
select的返回值:
select()调用返回处于就绪状态并且已经包含在fd_set结构中的描述字总数;
如果超时则返回0;否则的话,返回SOCKET_ERROR错误,通过WSAGetLastError获取相应错误代码。
当返回位0时,所有描述符集清0;
当返回为-1时,不修改任何描述符集;
当返回为非0时,在3个描述符集里,依旧是1的位就是准备好的描述符。这也就是为什么,每次用select后都要用FD_ISSET的原因。
三、简单实践
利用上述内容,实现一个基于TCP/IP的连接通信。
服务端:
//******************************************************************
- #include "stdafx.h"
- #include <iostream>
- #include <WinSock2.h>
- #pragma comment(lib, "WS2_32")
- using namespace std;
- # define REQUEST_BACKLOG 5
- //******************************
- //好吧不用写这些纠结的函数,就是看着清晰些
- //初始化Winsock
- bool InitWSA( const WORD &wVersion, WSADATA *wsadata )
- {
- int Ret = 0;
- if ( ( Ret = WSAStartup( wVersion,wsadata ) ) != 0 )
- {
- cout << "WSAStartup failed, error " << Ret << endl;
- return false;
- }
- return true;
- }
- //结束Winsock
- void cleanWSA()
- {
- if ( WSACleanup() == SOCKET_ERROR )
- {
- cout << "WSACleanup failed, error " << WSAGetLastError() << endl;
- }
- }
- //IPv4寻址,通过ip填充SOCKADDR_IN结构
- void InitSockAddrByIP( SOCKADDR_IN *pSockAddr, const char FAR *strIP, const INT &nPortID )
- {
- pSockAddr->sin_family = AF_INET;
- pSockAddr->sin_port = htons( nPortID );
- if ( 0 != strlen(strIP) )
- {
- pSockAddr->sin_addr.s_addr = inet_addr( strIP );
- }
- else
- {
- pSockAddr->sin_addr.s_addr = htonl( INADDR_ANY );
- }
- }
- //bind
- bool bindAddr( const SOCKADDR_IN *pSockAddr, SOCKET *pSocket )
- {
- int bindResult = bind( *pSocket, (sockaddr *)(pSockAddr), sizeof(*pSockAddr));
- if ( SOCKET_ERROR == bindResult )
- {
- cout << "bind error :" << WSAGetLastError() << endl;
- return false;
- }
- return true;
- }
- //listen
- bool setListener( SOCKET *pSocket, int backlog )
- {
- int nResult = listen( *pSocket, backlog );
- if ( SOCKET_ERROR == nResult )
- {
- cout << "listen error :" << WSAGetLastError() << endl;
- return false;
- }
- return true;
- }
- //******************************
- //程序入口
- int _tmain(int argc, _TCHAR* argv[])
- {
- //初始化Winsock
- WSADATA wsadata;
- if ( !InitWSA( MAKEWORD(2,2), &wsadata ) )
- {
- return 0;
- }
- //指定连接ip地址和服务器口
- SOCKADDR_IN InternetAddr;
- //char FAR strIP[] = "198.0.0.0";
- char FAR strIP[] = "";
- INT nPortID = 5150;
- InitSockAddrByIP( &InternetAddr, strIP, nPortID );
- //创建listener_socket
- SOCKET listener_socket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
- if ( INVALID_SOCKET == listener_socket )
- {
- cout << "listener_socket creat failed " << endl;
- return 0;
- }
- //bind
- if ( !bindAddr( &InternetAddr, &listener_socket ) )
- {
- return 0;
- }
- //监听
- if ( !setListener( &listener_socket, REQUEST_BACKLOG ) )
- {
- return 0;
- }
- cout << "server started~~~ " << endl;
- //创建 socket保存结构
- fd_set fdSocket;
- FD_ZERO( &fdSocket );
- FD_SET( listener_socket, &fdSocket );
- //查找可读的 socket
- while( true )
- {
- fd_set fdSocket_temp;
- fdSocket_temp = fdSocket;
- fd_set fdRead;
- fdRead = fdSocket;
- fd_set fdExceptds;
- fdExceptds = fdSocket;
- int nResult_select = select( 0, &fdRead, NULL, &fdExceptds, NULL );
- if ( 0 < nResult_select )
- {
- unsigned int socket_count = fdSocket_temp.fd_count;
- for ( unsigned int i=0; i< socket_count; i++ )
- {
- //可读的
- if ( FD_ISSET( fdSocket_temp.fd_array[i], &fdRead ) )
- {
- //找到所有可读连接
- if ( fdSocket_temp.fd_array[i] == listener_socket )
- {
- if ( fdSocket.fd_count < FD_SETSIZE )
- {
- //接受新的连接
- SOCKADDR_IN ClientAddr;
- int addrlen = static_cast<int>(sizeof(ClientAddr)); //一定要赋值
- SOCKET newClient_socket = accept( listener_socket, (sockaddr *)&ClientAddr, &addrlen );
- if ( INVALID_SOCKET == newClient_socket )
- {
- cout << " accept error " << WSAGetLastError() << endl;
- }
- else
- {
- FD_SET( newClient_socket, &fdSocket );
- cout << "find new connect: " << inet_ntoa( ClientAddr.sin_addr ) << endl;
- }
- }
- else
- {
- cout<<"too much connections"<<endl;
- continue;
- }
- }
- else
- {
- //接收数据
- char recvbuff[1024];
- int ret = 0;
- ret = recv( fdSocket_temp.fd_array[i], recvbuff, static_cast<int>( strlen(recvbuff) ), 0 );
- if ( 0 < ret )
- {
- recvbuff[ret] = '\0';
- cout << "recv : " << recvbuff << endl;
- //回复
- char backbuf[1024] = "receive info!";
- send( fdSocket_temp.fd_array[i], backbuf, static_cast<int>( strlen(backbuf) ), 0 );
- }
- else
- {
- //该连接断开
- closesocket( fdSocket_temp.fd_array[i] );
- FD_CLR( fdSocket_temp.fd_array[i], &fdSocket );
- }
- }
- }
- else if( fdSocket_temp.fd_array[i] != listener_socket )
- {
- //该连接断开
- closesocket( fdSocket_temp.fd_array[i] );
- FD_CLR( fdSocket_temp.fd_array[i], &fdSocket );
- }
- if ( FD_ISSET( fdSocket_temp.fd_array[i], &fdExceptds ) && (fdSocket_temp.fd_array[i] != listener_socket) )
- {
- //该连接断开
- closesocket( fdSocket_temp.fd_array[i] );
- FD_CLR( fdSocket_temp.fd_array[i], &fdSocket );
- }
- }//end for
- }
- else if( SOCKET_ERROR == nResult_select )
- {
- cout << "select error : " << WSAGetLastError() << endl;
- return 0;
- }
- Sleep( 50 ); //不要挑战你的机器
- }//end while
- closesocket( listener_socket );
- cleanWSA();
- return 0;
- }
客户端://******************************************************************
- #include "stdafx.h"
- #include <WinSock2.h>
- #include <iostream>
- #pragma comment(lib, "WS2_32")
- using namespace std;
- int _tmain(int argc, _TCHAR* argv[])
- {
- int result = 0;
- //初始化winsock
- WSADATA wsadata;
- result = WSAStartup( MAKEWORD(2,2), &wsadata );
- if ( 0 != result )
- {
- cout << "WSAStartup error " << result << endl;
- return 0;
- }
- //创建一个地址
- int serverPort = 5150;
- char FAR serverIP[] = "192.168.1.102"; //本机ip,不知道就ipconfig
- SOCKADDR_IN serverAddr;
- serverAddr.sin_family = AF_INET;
- serverAddr.sin_port = htons( serverPort );
- serverAddr.sin_addr.s_addr = inet_addr( serverIP );
- int serverAddr_size = static_cast<int>( sizeof(serverAddr) );
- //创建一个socket
- SOCKET socket_toServer;
- socket_toServer = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
- if ( INVALID_SOCKET == socket_toServer )
- {
- cout << "socket_toServer creat failed " << WSAGetLastError() << endl;
- return 0;
- }
- //连接
- result = connect( socket_toServer, (sockaddr *)&serverAddr, serverAddr_size );
- if ( SOCKET_ERROR == result )
- {
- cout << "connect error :" << WSAGetLastError() << endl;
- return 0;
- }
- char sendbuff[2048];
- char recvbuff[2048];
- while( true )
- {
- cout << "input send info:" << endl;
- cin >> sendbuff;
- int ret = send( socket_toServer, sendbuff, static_cast<int>( strlen(sendbuff) ), 0 );
- if ( SOCKET_ERROR == ret )
- {
- cout << "send error " << WSAGetLastError() << endl;
- break;
- }
- //处理接受的消息,由于之前没有accept和listen,这里使用recvfrom来接受
- int nRecv = 0;
- nRecv = recvfrom( socket_toServer, recvbuff, static_cast<int>( strlen(recvbuff) ), 0, (sockaddr *)&serverAddr, &serverAddr_size );
- if ( 0 < nRecv )
- {
- recvbuff[nRecv] = '\0';
- cout << "receive : " << recvbuff << endl;
- cout << "from : " << inet_ntoa( serverAddr.sin_addr ) << endl;
- cout << " " << endl;
- }
- }
- //清除各种数据和链接
- closesocket( socket_toServer );
- if ( SOCKET_ERROR == WSACleanup() )
- {
- cout << "WSACleanup error " << WSAGetLastError() << endl;
- return 0;
- }
- return 0;
- }
如题,上述内容只是帮助快速入门,要掌握Winsock还要经过大量的学习和实践。