Socket就是系统提供的一种使用传输层的网络协议进行数据传输的程序设计接口规范,经过Socket封装的应用程序不再需要处理与TCP协议相关的内容。诸如连接时的三次握手、分包、包头解析、重传、滑动窗口等行为应用程序都不需关注,而只需要像使用系统中的其他I/O接口一样进行输入和输出操作即可。
IP Helper是Windows系统与IP协议相关的配置和管理的重要接口,通过IP Helper可以实现一些在网络通信软件中常用的功能,比如获取本机IP网关,配置、获得统计信息、网卡数量和连接状态等。
Windows除了为网络通信程序设计提供Socket这样的传输层接口外,还提供了很多应用层协议接口,包括Http、SNMP等。此外还有DNS、DHCP等常用网络协议。
1、Socket通信
Socket套接字是使用传输程序协议(TCP、UDP等)进行网络通信的程序设计接口。Socket程序分为服务端与客户端,服务端程序监听端口,等待客户端程序的连接;客户端程序发起连接,等待服务端的响应。客户端程序需要知道服务端程序的IP地址和监听端口。服务端与客户端在建立连接后,双方都可以发送和接收数据。
所有接口函数都由ws2_32.dll导出,相关数据类型、结构定义、函数声明等位于头文件winsock2.h中。
1)客户端
客户端的功能是连接到服务端。Socket的连接是由客户端发起的;客户端在连接服务端的指定端口时如果指定端口开放,而且服务端响应了客户端的连接请求,连接就会建立。客户端在连接前需知道服务端程序所监听的端口和服务端程序所在主机的IP地址。在连接建立后,需要得到Socket,然后就可以向服务端发送数据,或者从服务端接收数据。
(1)客户端程序过程
一个Socket客户端程序的典型过程:
(1)客户端程序在运行后,首先需要使调用WSAStartup函数,确保进程加载socket应用程序所必须的环境和库文件,如Ws2_32.dll。
(2)调用函数Socket创建SOCKET,在创建时需指定使用的网络协议、连接类型等。
(3)填充SOCKADDR结构,指定服务端的地址、端口等。
(4)调用connect函数连接到服务端。
(5)如果连接成功,就可以使用send和recv函数发送和接收数据。
(6)在数据传输完成后,可调用closesocket函数关闭Socket。
(7)调用WSACleanup函数释放资源。[2]
系统在实现TCP协议时都为数据的接收保留了缓存。协议收到数据包后,解包并将数据放入缓存中,直到recv函数将数据接收。如果recv函数长时间不接收数据,在协议的缓存存满之后,对方的send函数就不能再发送数据。
当调用recv函数后,如果协议栈缓存中的数据还没有recv接收缓存的大,那么缓存中的数据有多少recv函数接收多少,然后返回。如果当前协议缓存中没有数据,那么一旦有数据到达就接收到达的数据,然后返回。如果协议缓存中的数据大于recv接收缓存,那么recv将接收缓存填满之后返回。
2)服务端
服务端的功能在指定的端口上监听,等待客户端的连接。在连接建立后可使用send和recv函数发送、接收数据。
一般情况下,Socket程序服务端过程如下:
(1)程序在运行后,首先需要使调用WSAStartup加载Ws2_32.dll。
(2)调用函数socket创建用于监听的SOCKET,在创建时需指定使用的网络协议、连接类型等。
(3)调用bind函数将Socket绑定到网络地址和端口。
(4)调用listen函数开始监听。
(5)调用accept函数等待客户端连接。在客户端连接后,accept函数返回,得到连接Socket。在accept函数返回后,可立即再调用,以处理其他客户端的连接。
(6)得到连接Socket后,可调用send和recv发送、接收数据。
(7)在数据传输完成后,可调用closesocket函数关闭Socket。
(8)调用WSACleanup函数释放DLL。
listen函数的功能是将Socket的状态设置为监听,以使客户端程序可以进行连接。accept函数的功能是接收客户端的连接,accept函数直到客户端有连接后才会返回。
3)相关API
Socket函数[3]
connect函数的功能是与服务端建立连接。这个函数只能由客户端程序调用。
示例Socket通信
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
**************************************/
/* 头文件 */
#include < stdio.h >
#include " winsock2.h "
/* 常量 */
#define RECV_BUFFER_SIZE 8192
#pragma comment(lib,"ws2_32.lib")
/* ************************************
* main
* 功能 socket通信客户端
************************************* */
void main( int argc, char * argv[])
{
// 变量定义
SOCKADDR_IN clientService; // 地址
SOCKET ConnectSocket; // socket
WSADATA wsaData; // 库
LPVOID recvbuf; // 接收缓存
int bytesSent;
int bytesRecv = 0 ;
char sendbuf[ 32 ] = " get information " ; // 默认发送的数据
// 初始化socket库, 保存ws2_32.dll已经加载
int iResult = WSAStartup(MAKEWORD( 2 , 2 ), & wsaData);
if (iResult != NO_ERROR)
printf( " Error at WSAStartup()\n " );
// 创建socket
ConnectSocket = socket(AF_INET, // IPv4
SOCK_STREAM, // 顺序的、可靠的、基于连接的、双向的数据流通信
IPPROTO_TCP // 使用TCP协议
);
if (ConnectSocket == INVALID_SOCKET)
{
printf( " Error at socket(): %ld\n " , WSAGetLastError());
WSACleanup();
return ;
}
// 设置服务端的通信协议、IP地址、端口
clientService.sin_family = AF_INET;
clientService.sin_addr.s_addr = inet_addr( " 127.0.0.1 " );
clientService.sin_port = htons( 10000 );
// 连接到服务端
if ( connect(
ConnectSocket, // socket
(SOCKADDR * ) & clientService, // 地址
sizeof (clientService) // 地址的大小
) == SOCKET_ERROR)
{
printf( " Failed to connect(%d)\n " ,WSAGetLastError() );
WSACleanup();
return ;
}
// 准备发送数据
// 如果输入参数是-d,那么发送的数据是“download file”否则是"get information"
if (argc == 2 && ( ! lstrcmp(argv[ 1 ], " -d " )))
{
lstrcpyn(sendbuf, " download file " , 32 );
}
// 向服务端发送数据
bytesSent = send( ConnectSocket, // socket
sendbuf, // 发送的数据
lstrlen(sendbuf) + 1 , // 数据长度
0 ); // 无标志
if (bytesSent == SOCKET_ERROR)
{
printf( " send error (%d)\n " , WSAGetLastError());
closesocket(ConnectSocket);
return ;
}
printf( " Bytes Sent: %ld\n " , bytesSent );
// 准备接收数据
recvbuf = HeapAlloc(GetProcessHeap(), 0 , RECV_BUFFER_SIZE);
// 循环接收
while ( bytesRecv != SOCKET_ERROR )
{
// Sleep(50);
bytesRecv = recv( ConnectSocket, // socket
( char * )recvbuf, // 接收数据缓存
RECV_BUFFER_SIZE, // 缓存大小
0 ); // 无标志
if ( bytesRecv == 0 )
{
printf( " Connection Closed.\n " );
break ;
}
// TODO,处理接收的数据,这里只简单的将收到的数据大小显示
printf( " Bytes Recv: %ld\n " , bytesRecv );
}
HeapFree(GetProcessHeap(), 0 , recvbuf);
WSACleanup();
return ;
}
* server.c
**************************************/
/* 头文件 */
#include < winsock2.h >
#include < ws2tcpip.h >
#include < stdio.h >
/* 常量 */
#define DEFAULT_PORT "10000" // 端口
#define MAX_REQUEST 1024 // 接收数据的缓存大小
#define BUF_SIZE 4096 // 发送数据的缓存大小
#pragma comment(lib,"ws2_32.lib")
/* ************************************
* CommunicationThread
* 功能 用于接收和发送数据的线程
* 为每一个连接的客户端创建一个接收发送数据的线程,
* 可以使用多个客户端同时连接到服务端
* 参数 lpParameter,SOKCET
************************************* */
DWORD WINAPI CommunicationThread(
LPVOID lpParameter
)
{
DWORD dwTid = GetCurrentThreadId();
// 获得参数sokcet
SOCKET socket = (SOCKET)lpParameter;
// 为接收数据分配空间
LPSTR szRequest = (LPSTR)HeapAlloc(GetProcessHeap(), 0 , MAX_REQUEST);
int iResult;
int bytesSent; // 用于保存send的返回值,实际发送的数据的大小
// 接收数据
iResult = recv(socket, // socket
szRequest, // 接收缓存
MAX_REQUEST, // 缓存大小
0 ); // 标志
if (iResult == 0 ) // 接收数据失败,连接已经关闭
{
printf( " Connection closing...\n " );
HeapFree(GetProcessHeap(), 0 ,szRequest);
closesocket(socket);
return 1 ;
}
else if (iResult == SOCKET_ERROR) // 接收数据失败,socket错误
{
printf( " recv failed: %d\n " , WSAGetLastError());
HeapFree(GetProcessHeap(), 0 ,szRequest);
closesocket(socket);
return 1 ;
}
else if (iResult > 0 ) // 接收数据成功
{
// 显示接收到的数据
printf( " \tCommunicationThread(%d)\tBytes received: %d\n " , dwTid, iResult);
printf( " \tCommunicationThread(%d)\trequest string is (%s)\n " ,dwTid, szRequest);
// 如果接收到的数据是"download file"
if (lstrcmpi(szRequest, " download file " ) == 0 )
{
// 读取文件download.txt将发送
HANDLE hFile;
LPVOID lpReadBuf; // 发送缓存
DWORD dwBytesRead;
DWORD dwFileSize;
DWORD dwSendFile = 0 ;
hFile = CreateFile( " download.txt " ,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf( " \tCommunicationThread\tCould not open file (error %d)\n " ,
GetLastError());
send(socket, " error " , 6 , 0 );
closesocket(socket);
return 1 ;
}
// 分配发送数据缓存
lpReadBuf = HeapAlloc(GetProcessHeap(), 0 , BUF_SIZE);
// 获取文件大小
dwFileSize = GetFileSize(hFile, NULL);
// 循环发送
while ( 1 )
{
// 读文件到缓存
if ( ! ReadFile(hFile, lpReadBuf, BUF_SIZE, & dwBytesRead, NULL))
{
printf( " \tCommunicationThread\tCould not read from file (error %d)\n " ,
GetLastError());
closesocket(socket);
CloseHandle(hFile);
return 1 ;
}
// 发送读取的文件数据
bytesSent = send(socket, ( const char * )lpReadBuf, dwBytesRead, 0 );
if ( bytesSent == SOCKET_ERROR)
{
printf( " \tCommunicationThread\tsend error %d\n " ,
WSAGetLastError());
closesocket(socket);
CloseHandle(hFile);
return 1 ;
}
// 显示发送数据的大小
printf( " \tCommunicationThread(%d)\tsend %d bytes\n " , dwTid, bytesSent);
// 累加,已经发送的大小
dwSendFile += dwBytesRead;
// 如果所有文件数据都已经发送
if (dwSendFile == dwFileSize)
{
printf( " \tCommunicationThread\tFile download ok\n " );
break ; // 退出循环
}
}
// 释放内存、关闭连接,关闭文件
HeapFree(GetProcessHeap(), 0 , lpReadBuf);
CloseHandle(hFile);
closesocket(socket);
}
// 如果接收到的数据是"get information"
else if (lstrcmpi(szRequest, " get information " ) == 0 )
{
// 发送数据
bytesSent = send(socket, // socket
" this is information " , // 数据
lstrlen( " this is information " ) + 1 , // 数据长度
0 ); // 标志
// 判断是否成功
if ( bytesSent == SOCKET_ERROR)
{
printf( " \tCommunicationThread\tsend error %d\n " ,
WSAGetLastError());
closesocket(socket);
return 1 ;
}
printf( " \tCommunicationThread(%d)\tsend %d bytes\n " ,dwTid, bytesSent);
}
else // 收到未知数据
{
printf ( " unreferenced request\n " );
}
}
// 释放接收数据缓存,关闭socket
HeapFree(GetProcessHeap(), 0 ,szRequest);
closesocket(socket);
return 0 ;
}
/* ************************************
* int __cdecl main(void)
* 功能 socket服务端
************************************* */
int __cdecl main( void )
{
WSADATA wsaData;
SOCKET ListenSocket = INVALID_SOCKET; // 监听socket
SOCKET ClientSocket = INVALID_SOCKET; // 连接socket
struct addrinfo * result = NULL,
hints;
int iResult; // 保存返回结果
// 初始化Winsock,保证Ws2_32.dll已经加载
iResult = WSAStartup(MAKEWORD( 2 , 2 ), & wsaData);
if (iResult != 0 )
{
printf( " WSAStartup failed: %d\n " , iResult);
return 1 ;
}
// 地址
ZeroMemory( & hints, sizeof (hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
// 获取主机地址,保证网络协议可用等
iResult = getaddrinfo(NULL, // 本机
DEFAULT_PORT, // 端口
& hints, // 使用的网络协议,连接类型等
& result); // 结果
if ( iResult != 0 )
{
printf( " getaddrinfo failed: %d\n " , iResult);
WSACleanup();
return 1 ;
}
// 创建socket,用于监听
ListenSocket = socket(
result -> ai_family, // 网络协议,AF_INET,IPv4
result -> ai_socktype, // 类型,SOCK_STREAM
result -> ai_protocol); // 通信协议,TCP
if (ListenSocket == INVALID_SOCKET)
{
printf( " socket failed: %ld\n " , WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1 ;
}
// 绑定到端口
iResult = bind( ListenSocket, result -> ai_addr, ( int )result -> ai_addrlen);
if (iResult == SOCKET_ERROR)
{
printf( " bind failed: %d\n " , WSAGetLastError());
freeaddrinfo(result);
closesocket(ListenSocket);
WSACleanup();
return 1 ;
}
printf( " bind\n " );
freeaddrinfo(result); // reuslt不再使用
// 开始监听
iResult = listen(ListenSocket, SOMAXCONN);
printf( " start listen......\n " );
if (iResult == SOCKET_ERROR)
{
printf( " listen failed: %d\n " , WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1 ;
}
while ( 1 )
{
// 接收客户端的连接,accept函数会等待,直到连接建立
printf( " ready to accept\n " );
ClientSocket = accept(ListenSocket, NULL, NULL);
// accept函数返回,说明已经有客户端连接
// 返回连接socket
printf( " accept a connetion\n " );
if (ClientSocket == INVALID_SOCKET)
{
printf( " accept failed: %d\n " , WSAGetLastError());
closesocket(ListenSocket);
break ; // 等待连接错误,退出循环
}
// 为每一个连接创建一个数据发送的接收线程,
// 使服务端又可以立即接收其他客户端的连接
if ( ! CreateThread(
NULL,
0 ,
CommunicationThread, // 线程函数
(LPVOID)ClientSocket, // 将socket作为参数
0 ,
NULL))
{
printf( " Create Thread error (%d) " , GetLastError());
break ;
}
}
// 循环退出,释放DLL。
WSACleanup();
return 0 ;
}
4)处理并发的客户端连接
服务端在监听时需指定一个端口,这个端口可以接收多个客户端的连接。客户端在连接服务端时,本机的端口不需要指定,系统会自动选取一个当前不用的端口与服务端的固定端口连接。
在同一时刻可以有多个不同主机上的客户端连接到服务端,一个主机上也可以同时有多个客户端连接到同一个服务端,但是在建立连接时,socket函数会为客户端分配不同的端口。客户端不会使用同一端口和同一服务端建立多次连接。
服务端程序使用accept函数接收服务端的连接。因此如果服务端需要有与多个客户端连接时,在accept函数返回,一个连接已经建立后需要立即再调用accept函数,等待其他客户端的连接。如果客户端连接时,而服务端程序此时如果没有调用accept函数,那么连接不会成功建立。
因此处理并发客户端连接的服务端必定是多线程的。监听程序最好使用单独的线程,而且为了对每个客户端的请求都能立即响应,每个与客户端的连接最好建立至少有一个线程处理数据的发送和接收。
进行网络数据的发送和接收不仅只有send和recv函数可以使用,还可以使用sentto、recvform、WSARecv、WSARecvEx等来进行异步连接。
2、IP Helper[1,P482]
参考
[1] 精通Windows API 函数、接口、编程实例
[2] http://msdn.microsoft.com/en-us/library/ms741412%28VS.85%29.aspx
[3] http://msdn.microsoft.com/en-us/library/ms740506%28VS.85%29.aspx