本节通过具体实例讲述如何建立socket通信。
TCP/IP协议
关于TCP/IP协议的基础内容建议直接阅读谢希仁的《计算机网络》,里面有非常详细的讲解,在此不做过多赘述,本节主要介绍网络通信编程。
常见的数据包格式——IP数据包
IP数据包是在IP协议间发送的,主要在以太网与网际协议模块之间传输,提供无连接数据包传输。IP协议不保证数据包的发送,但最大限度地发送数据。IP协议结构定义如下:
typedef struct HeadIP{
unsigned char headerlen:4;//首部长度,占4位
unsigned char version:;//版本,占4位
unsigned char servertype;//服务类型,占8位,即一个字节
unsigned short totallen;//总长度,占16位
unsigned short id;//与idoff构成标识,共占16位,前三位是标识,后13位是片偏移
unsigned short idoff;
unsigned char ttl;//生存时间,占8位
unsigned char proto;//协议,占8位
unsigned short checksum;//首部校验和,占16位
unsigned int sourceIP;//源IP地址,占32位
unsigned int destIP;//目的IP地址
}HEADIP;
常见的数据包格式——TCP数据包
TCP提供一个完全可靠的,面向连接的,全双工的流传输服务。
typedef struct HeadTCP
{
WORD SourcePort;//16位源端口号
WORD Deport;//16位目的端口
DWORD SequenceNo;//32位序号
DWORD ConfirmNo;//32位确认序号
BYTE HeadLen;//与flag为一个组成部分,首部长度,占4位,保留6位,6位标识,共16位
WORD WndSize;//16位窗口大小
WORD CheckSum;//16位校验和
WORD UrgPtr;//16位紧急指针
}HEADTCP;
常见的数据包格式——UDP数据包
UDP是一个面向无连接的协议,采用该协议的应用程序不需要先建立连接。它为应用程序提供一次性的数据传输服务。
typedef struct HeadUDP
{
WORD SourcePort;//16位源端口号
WORD Deport;//16位目的端口
WORD Len;//16位UDP长度
WORD CheckSum;//16位UDP校验和
}HEADUDP;
常见的数据包格式——ICMP数据包
ICMP协议被称为网际控制报文协议。作为IP协议的附属协议,ICMP协议用来与其他主句或者路由器交换错误报文和其他重要信息,可以将某个设备的故障信息发送到其他设备上。
typedef struct HeadICMP
{
BYTE Type;//8位类型
BYTE Code;//8位代码
WORD CheckSum;//16位UDP校验和
}HEADICMP;
套接字
套接字本质上是一个指向传输提供者的句柄。在Winsock中,就是通过操作该句柄来实现网络通信和管理。根据性质和作用的不同,套接字可以分为3种,即
- 原始套接字:是在 Winsock2规范中提出的,它能够使程序开发人员对底层的网络传输机制进行控制,在原始套接字下接收的数据中含有IP头;
- 流式套接字:提供了双向、有序、可靠的数据传输服务,该类型套接字在通信前需要双方建立连接接(TCP协议);
- 数据包套接字:提供双向的数据流,但是不保证数据传输的可靠性,有序性和无重复性(UDP协议)。
Winsocket套接字
套接字是网络通信的基石,Winsocket2.0允许多种协议栈的并存,可以使应用程序适用于不同的网络名和网络地址。
Winsocket系统提供的套接字函数通常封装在Ws_32.dll动态链接库中,头文件Winsocket2.h提供了套接字函数的原型,库文件Ws_32.lib提供了Ws2_32.dll动态链接库的输出节。在使用套接字函数前,用户需要引用Winsocket2.h头文件,并链接Ws2_32.dll库文件。
#include <winsock.h>//引用头文件
#pragma comment(lib,"ws2_32.lib")//链接库文件
使用套接字函数前需要初始化套接字WSAStartup函数来实现。例如:
WSADATA wsd;//定义WSADATA对象
WSAStartup(MAKEWORD(2, 2), &wsd);//初始化套接字
常用套接字函数:
WSAStartup函数:
用于初始化Ws2_32.dll动态链接库,在使用套接字函数之前,一定要进行初始化
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
wVersionRequested:表示使用的Windows Socket的版本
lpWSAData:是一个WSADATA结构指针,该结构详细记录了Windows套接字的相关信息,其定义方式如下:
#include <winsock.h>
typedef struct WSAData
{
WORD wVersion;//调用者使用的Ws2_32.dll动态库版本号
WORD wHighVersion;//Ws2_32.dll支持的最高版本
char szDescription [WSADESCRIPTION_LEN + 1];//套接字的详细信息,没有实际意义
char szSystemStatus[WSASYS_STATUS_LEN + 1];//系统的配置或者状态信息,没有实际意义
unsigned short iMaxSockets;//最多可以打开多少个套接字,忽略
unsigned short iMaxUdpDg;//数据包的最大长度,忽略
char FAR* lpVendorinfo;//套接字的厂商信息,忽略
}WSAData,FAR* LPWSADATA;
socket函数:
该函数用于创建一个套接字。
SOCKET socket(int af, int type, int protocol);
af:一个地址家族,通常为AF_INET;
type:套接字类型。SOCK_STREAM表示创建流式套接字,SOCK_DGRAM表示创建无连接的数据报套接字,SOCK_RAW表示创建原始套接字;
protocol:套接口所用的协议;
函数返回值是创建套接字的套接字句柄。
bind函数:
该函数用于将套接字绑定到指定的端口和地址上
int bind(SOCKET s, const struct sockaddr FAR* name, int namelen);
s:套接字标识;
name:一个sockaddr结构指针,包含了要结合的地址和端口号;
namelen:确定name缓冲区的长度;
如果函数执行成功返回0,否则返回SOCKET_ERROR。
listen函数:
该函数用于将套接字设置为监听模式,对于流式套接字,必须处于监听模式才能够接收客户端套接字的链接
int listen(SOCKET s, int backlog);
s:套接字标识;
backlog:等待连接的最大队列长度;
int listen(SOCKET s, int backlog);
accept函数:
该函数用于接收客户端的连接,在流式套接字中,套接字处于监听状态才能接收客户端的连接。
int SOCKET(SOCKET s, struct sockaddr FAR* name, int FAR* addrlen);
s:一个套接字,它应该处于监听状态;
name:一个sockaddr_in结构指针,包含了一组客户端的端口号和IP地址等信息;
addrlen:用于接受剖参数的addr长度;
返回值是一个新的套接字,它对应于已经接受的客户端连接,对于客户端的所有后续操作,都应该使用这个新的套接字。
closesocket函数:
该函数用于关闭套接字。
int closesocket(SOCKET s);
s:套接字标识,如果参数有SO_DONTLINGER选项,调用该函数后立即返回,若此时如果有数据未传送完毕,会继续传送,然后再关闭套接字。
connect函数:
该函数用于发送一个连接请求
int connect(SOCKET s, const struct sockaddr FAR* name, int namelen);
s:是一个套接字;
name:套接字s想要连接的主机地址和端口号;
namelen:name缓冲区的长度;
如果函数执行成功返回0,否则返回SOCKET_ERROR。
htons函数:
该函数将一个16位的无符号短整型数据由主机排列方式转换为网络排列方式
u_short htons(u_short hostshort);
hostshort:一个主机排列方式的无符号短整型数据;
返回值:是16为位的网络排列方式数据。
htoml函数:
该函数将一个无符号长整型数据由主机排列方式转换为网络排列方式
u_short htonl(u_short hostslong);
hostlong:一个主机排列方式的无符号长整型数据;
返回值:是32位的网络排列方式数据。
inet_addr函数:
该函数将一个由字符串表示的地址转换为32位无符号长整型数据
unsigned long inet_addr(const chat FAR* cp);
cp:一个IP地址的字符串
返回值:32位的无符号长整型。
recv函数:
该函数用于从面向连接的套接字中接收数据
int recv(SOCKET s, char FAR* buf, int len, int flags);
s:一个套接字;
buf:接收数据的缓冲区;
len:buf的长度;
flags:函数的调用方式。
send函数:
该函数用于从面向连接的套接字间发送数据
int send(SOCKET s, char FAR* buf, int len, int flags);
s:一个套接字;
buf:发送数据的缓冲区;
len:buf的长度;
flags:函数的调用方式。
select函数:
该函数用来检查一个或多个套接字是否处于可读、可写或错误状态
int select(int nfds, fd_set FAR* readfds, fd_set FAR* writefds, fd_set FAR*exceptfds, const struct timeval FAR* timeout);
nfds:无实际意义,只是为了和UNIX下的套接字兼容
readfds:一组被检查可读的套接字
writefds:一组被检查可写的套接字
exceptfds:被检查有错误的套接字
timeout:函数的等待时间
WSACleanup函数:
该函数用于释放为Ws2_32.dll动态链接库初始化时分配的资源。
int WSACleanup(void);
WSAAsyncSelect函数:
int WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent);
s:一个套接字;
hWnd:接收消息的窗口句柄;
wMsg:窗口接收来自套接字中的消息;
lEvent:网络中发生的事件。
ioctlsocket函数
该函数用于设置套接字的I/O模式
int ioctlsocket(SOCKET s, long cmd, u_long FAR* argp);
s:待更改I/O模式的套接字;
cmd: 对套接字的操作命令;
argp:命令参数
面向连接流
面向连接流主要指通信双方在通信前要先建立连接,建立连接的步骤如下:
1)创建套接字(socket)
2)将创建的套接字绑定(bind)到本地的地址和端口上
3)服务端设置套接字的状态为监听状态(listen),准备接受客户端的连接请求
4)服务端接受请求(accept),同时返回得到一个用于连接的新套接字
5)使用这个新套接字进行通信(通信函数使用send/recv)
6)释放套接字资源(closesocket)
整个过程分为客户端和服务端,两端连接过程如图所示
面向无连接流
主要指通信双方通信前不需要建立连接,服务端和客户端使用相同的处理过程,如图所示:
简单协议的通信过程
下面代码实例主要完成一个简单的通信过程,使用的是面向连接方式建立的连接,并且是阻塞的方式。包括客户端和服务端。
服务端
服务端主要使用多线程技术建立连接,一个服务端可以连接多个客户端,连接客户端的数据可以进行限定,程序中设置最大连接数为20.当客户端有请求发过来时,向客户端发送字符串”THIS IS SERVER“,并启动一个线程等待客户端发送消息过来。
如果客户端发送字符”A“,服务器返回B,发送字符”C“过来,服务器返回”D“,发送”exit“后,服务器关闭线程。
#include <iostream>
#include <stdlib.h>
#include "winsock2.h"//引用头文件
#pragma comment (lib,"ws2_32.lib")//引用库文件
using namespace std;
//定义线程实现函数
//DWORD 就是 Double Word, 每个word为2个字节的长度,DWORD 双字即为4个字节,每个字节是8位,共32位,经常用来保存地址(或者存放指针)
//WINAPI 函数的一种调用方式,threadpro 一个线程的起始地址
DWORD WINAPI threadpro(LPVOID pParam)
{
SOCKET hsock = (SOCKET)pParam;//定义一个套接字
char buffer[1024] = { 0 };//缓存
char sendBuffer[1024];//发送缓存
if (hsock != INVALID_SOCKET)//INVALID_SOCKET 表示socket无效
{
cout << "Start Receive" << endl;
}
//循环接收发送内容
while (1)
{
int num = recv(hsock, buffer, 1024, 0);//阻塞函数,等待接收内容
if (num >= 0)//返回值>0,准备开始接收
{
cout << "Receive from clinet" << buffer << endl;
}
cout << WSAGetLastError() << endl;//返回值指示此线程最后一次失败的 Windows 套接字操作的错误代码
if (!strcmp(buffer, "A"))//如果buffer == A
{
memset(sendBuffer, 0, 1024);
strcpy(sendBuffer, "B");//把"B"放入发送缓存
int ires = send(hsock, sendBuffer, sizeof(sendBuffer), 0);//回送信息
cout << "Send to client" << sendBuffer << endl;
}
else if(!strcmp(buffer,"C"))//如果buffer == C
{
memset(sendBuffer, 0, 1024);
strcpy(sendBuffer, "D");//把"D"放入发送缓存
int ires = send(hsock, sendBuffer, sizeof(sendBuffer), 0);//回送信息
cout << "Send to client" << sendBuffer << endl;
}
else if (!strcmp(buffer, "exit"))
{
cout << "Client Close" << endl;
cout << "Server Process Close" << endl;
return 0;
}
else
{
memset(sendBuffer, 0, 1024);
strcpy(sendBuffer, "ERR");
int ires = send(hsock, sendBuffer, sizeof(sendBuffer), 0);
cout << "Send to client" << sendBuffer << endl;
}
}
return 0;
}
//主函数
void main()
{
WSADATA wsd;//定义WSADARTA对象:WSADATA包含有关 Windows 套接字实现的信息
//调用WSAStartup,初始化Winsock
DWORD err = WSAStartup(MAKEWORD(2, 2), &wsd);
cout << err << endl;//WSAStartup返回值,若成功,返回零,否则返回错误代码
SOCKET m_SocketServer;//定义一个套接字
sockaddr_in serveraddr;//网络地址结构
sockaddr_in serveraddrfrom;
SOCKET m_Server[20];
serveraddr.sin_family = AF_INET;//指代协议族,在socket编程中只能是AF_INET
serveraddr.sin_port = htons(4600);//设置服务器端口号
serveraddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//存储Ip地址
//调用socket(),创建一个会话Socket
m_SocketServer = socket(AF_INET, SOCK_STREAM, 0);//流式套接字,函数返回值是创建的套接字句柄
//调用bind()为Socket制定通信对象
int i = bind(m_SocketServer, (sockaddr*)&serveraddr, sizeof(serveraddr));//该函数用于将套接字绑定到指定的端口和地址上
//函数执行成功就返回0,否则返回SOCKET_ERROR
cout << "bind:" << i << endl;
int iMaxConnect = 20;//最大连接数
int iConnect = 0;
int iLisRet;
char buf[] = "THIS IS SERVER\0";//向客户端发送的内容
char WarmBuf[] = "It is voer Max connect\0";
int len = sizeof(sockaddr);
while (1)
{
//调用listen()设置登台连接状态
iLisRet = listen(m_SocketServer,0);//进行监听
//调用accept()接收连接并生成会话socket
//同意建立连接
m_Server[iConnect] = accept(m_SocketServer, (sockaddr*)&serveraddrfrom, &len);//返回一个新的套接字,它对应于已经接受的客户端连接
if (m_Server[iConnect] != INVALID_SOCKET)
{
//调用send进行对话,发送字符过去
int ires = send(m_Server[iConnect], buf, sizeof(buf), 0);
cout << "accept" << ires << endl;//显示已经建立连接次数
iConnect++;
if (iConnect > iMaxConnect)
{
int ires = send(m_Server[iConnect], WarmBuf, sizeof(WarmBuf), 0);
}
else
{
HANDLE m_Handle;//线程句柄
DWORD nThreadld = 0;//线程ID
m_Handle = (HANDLE)::CreateThread(NULL,0,threadpro, (LPVOID)m_Server[--iConnect], 0, &nThreadld);//启动线程
}
}
}
//关闭Socket
WSACleanup();
}
客户端
客户端程序主要完成向服务器端发送请求连接,然后由用户输入要发送的字符,发送的字符限定在A,C和exit。
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "winsock2.h"//引用头文件
#pragma comment(lib,"ws2_32.lib")//引用库文件
using namespace std;
void main()
{
WSADATA wsd;//定义WSADARTA对象:WSADATA包含有关 Windows 套接字实现的信息
//调用WSAStartup,初始化Winsock
WSAStartup(MAKEWORD(2, 2), &wsd);
SOCKET m_SockClient;//定义一个客户端套接字
sockaddr_in clientaddr;//客户端网络地址结构
clientaddr.sin_family = AF_INET;//指代协议族,在socket编程中只能是AF_INET
clientaddr.sin_port = htons(4600);//设置服务器端口号
clientaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
//调用socket(),创建一个会话Socket
m_SockClient = socket(AF_INET, SOCK_STREAM, 0);
//调用connect()与Server端连接
int i = connect(m_SockClient, (sockaddr*)&clientaddr,sizeof(clientaddr));
cout << "connect" << i << endl;
char buffer[1024];
char inBuf[1024];
int num;
//调用recv开始会话
num = recv(m_SockClient, buffer, 1024, 0);
if (num > 0)
{
cout << "Receive from server" << buffer << endl;
while (1)
{
num = 0;
cin >> inBuf;
if (!strcmp(inBuf, "exit"))//发送退出指令
{
send(m_SockClient, inBuf, sizeof(inBuf), 0);
return;
}
send(m_SockClient, inBuf, sizeof(inBuf), 0);
num = recv(m_SockClient, buffer, 1024, 0);//接收客户端发送过来的数据
if (num > 0)
{
cout << "Receive from server" << buffer << endl;
}
}
}
}
运行方式:先启动服务端程序,再启动客户端程序。运行结果如下:
服务器端
客户端