昔洛 的个人博客

Bug不空,誓不成佛

  menu
70 文章
14633 浏览
32 当前访客
ღゝ◡╹)ノ❤️

Windows 网络编程-Winsock (一)

Windows网络编程套接字的类型

套接字的类型:

  • 流式套接字(SOCK_STREAM):提供面向连接、可靠的数据传输服务,数据无差错、无重复的发送,且发送顺序接收
  • 数据报式套接字(SOCK_DGRAM):提供无连接服务。数据包以独立包形式发送,不提供无措保证,数据可能丢失或重复,并且接收顺序混乱
  • 原始套接字(SOCK_RAW):WinSock接口并不使用某种特定的协议去封装它,而是有程序自行处理数据报以及协议首部

基于TCP(面向连接)的 socket 编程

一、服务端程序

  1. 创建套接字(socket)。
  2. 将套接字绑定到一个本地地址和端口上(bind)。
  3. 将套接字设为监听模式,准备接受客户端请求(client)。
  4. 等待客户端请求到来,当请求到来后,接收连接请求,返回一个新的对应于此次连接的套接字(accept)。
  5. 用返回的套接字和客户端进行通信(send/recv)。
  6. 返回,等待另一个客户请求。
  7. 关闭套接字。

二、客户端程序:

  1. 创建套接字(socket)。
  2. 向服务器发送连接请求(connect)。
  3. 和服务器端进行通信(send/recv)。
  4. 关闭套接字。

三、一个简单的通信案例

① 打开VC6.0创建一个新的空的控制台项目
② 为项目添加wsock32.lib 链接库,可使用
#pragma comment(lib,"wsock32.lib")
或者 Project->Settings->Link 在Object/library modules 添加 ws2_32.lib
③ 创建一个TcpSrv.cpp用来编写服务端程序,详细代码如下:

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib,"wsock32.lib")

int main()
{
	//WSAStartup的参数
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	
	wVersionRequested = MAKEWORD( 1, 1 );
//将地位和高位连接起来创建一个WORD类型的值
	
	/*WSAstartup必须是应用程序首先调用的WInsock函数,
它允许应用程序指定所需的Windows Sockets API 的版本,获取特定WInsock实现的详细信息。
仅当这个函数成功执行之后,应用程序才能调用其他WInsock API*/
	err = WSAStartup( wVersionRequested, &wsaData );
	if ( err != 0 ) {
//执行成功后err应该是0,否则出错要调用 WSAGetLastError 函数查看出错的原因。
		return 0;
	}
	
	//如果获取的版本号的地位高位不是所设置的,那么使用WSACleanup进行释放
//每个WSAStartup的调用必须对应一个对WSACleanup的调用,这个函数释放WInsock库
	if ( LOBYTE( wsaData.wVersion ) != 1 ||
        HIBYTE( wsaData.wVersion ) != 1 ) {
		WSACleanup( );
		return 0; 
	}
	
	//创建套接字对象
	SOCKET sockSrv = socket(AF_INET,SOCK_STREAM,0);

	
	//设置将要绑定的地址和端口号
	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(6000);
	
	//绑定套接字到指定的IP地址和端口号
	bind(sockSrv,(SOCKADDR *)&addrSrv,sizeof(SOCKADDR));

	
	//设置套接字进入监听状态
	listen(sockSrv,5);

	SOCKADDR_IN addrClient;
	int len = sizeof(SOCKADDR);
	//循环监听
	while(TRUE)
	{
		//等待接收客户端的连接
		SOCKET sockConn = accept(sockSrv,(SOCKADDR *)&addrClient,&len);
		char sendBuf[100];
		//格式化字符串,将要发送给客户端
		sprintf(sendBuf,"Welcome %s to my server",inet_ntoa(addrClient.sin_addr));
		//向客户端发送信息
		send(sockConn,sendBuf,strlen(sendBuf)+1,0);
		char recvBuf[100];
		//接收客户端的消息
		recv(sockConn,recvBuf,100,0);
		printf("%s\n",recvBuf);
		//关闭socket套接字
		closesocket(sockConn);
	}
	return 0;
}

④ 创建一个新的空的控制台工程,添加lib后,创建一个TcpClient.cpp用于编写客户端连接程序,代码如下:

#include <winsock2.h>
#include <stdio.h>

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	
	wVersionRequested = MAKEWORD( 1, 1 );
	
	err = WSAStartup( wVersionRequested, &wsaData );
	if ( err != 0 ) {
		return 0;
	}
	
	if ( LOBYTE( wsaData.wVersion ) != 1 ||
        HIBYTE( wsaData.wVersion ) != 1 ) {
		WSACleanup( );
		return 0; 
	}

	//以上代码均为WInsock编程必要的步骤
	
	//创建客户端socket 
	SOCKET sockClient = socket(AF_INET,SOCK_STREAM,0);

	//设置socket连接的地址和端口
	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port=htons(6000);

	//进行连接
	connect(sockClient,(SOCKADDR *)&addrSrv,sizeof(SOCKADDR));
	
	char recvBuf[100];
	//接收消息
	recv(sockClient,recvBuf,100,0);
	printf("%s\n",recvBuf);

	char sendBuf[100] = "你好,网络世界!";
	//发送消息
	send(sockClient,sendBuf,sizeof(sendBuf)+1,0);

	
	//关闭socket
	closesocket(sockClient);
	//清除资源
	WSACleanup();
	return 0;
}

运行结果如下:
Server:
image.png
Client:
image.png

基于UDP(面向无连接)的 socket 编程

一、服务器端(接收端)程序:

① 创建套接字(socket)
② 将套接字绑定到一个本地地址和端口上(bind)
③ 等待接收数据(recvfrom)
④ 关闭套接字

二、客户端(发送端)程序:

① 创建套接字(socket)
② 向服务器发送数据(sendto)
③ 关闭套接字

三、UDP 编程的简单例子

创建工程步骤略
服务器端代码:

#include <winsock2.h>
#include <stdio.h>

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	
	wVersionRequested = MAKEWORD( 1, 1 );
	
	err = WSAStartup( wVersionRequested, &wsaData );
	if ( err != 0 ) {
		return 0;
	}
	
	if ( LOBYTE( wsaData.wVersion ) != 1 ||
        HIBYTE( wsaData.wVersion ) != 1 ) {
		WSACleanup( );
		return 0; 
	}
	
//以上代码为Winsocket编程必要步骤,说明略

	//创建服务端socket,并设置端口
	SOCKET sockSrv = socket(AF_INET,SOCK_DGRAM,0);
	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(6000);


	//绑定socket信息
	bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));

	SOCKADDR_IN addrClient;
	int len = sizeof(SOCKADDR);
	char recvBuf[100];
	//接收客户端信息
	recvfrom(sockSrv,recvBuf,strlen(recvBuf),0,(SOCKADDR*)&addrClient,&len);
	printf("%s\n",recvBuf);
	//关闭socket
	closesocket(sockSrv);
	//清除资源
	WSACleanup();
	return 0;
}

客户端程序:

#include <winsock2.h>
#include <stdio.h>

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	
	wVersionRequested = MAKEWORD( 1, 1 );
	
	err = WSAStartup( wVersionRequested, &wsaData );
	if ( err != 0 ) {
		return 0;
	}
	
	if ( LOBYTE( wsaData.wVersion ) != 1 ||
        HIBYTE( wsaData.wVersion ) != 1 ) {
		WSACleanup( );
		return 0; 
	}

	//以上代码为Winsocket编程必要步骤,说明略

	//创建客户端socket
	SOCKET sockClient = socket(AF_INET,SOCK_DGRAM,0);
	SOCKADDR_IN addrSrv;
	//设置发送地址
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	addrSrv.sin_family = AF_INET;
	//发送端口
	addrSrv.sin_port = htons(6000);

	char sendBuf[100] = "你好啊,UDP世界";
	//发送数据报
	sendto(sockClient,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
	//关闭socket
	closesocket(sockClient);
	//清除资源
	WSACleanup();
	return 0;
}

运行结果如下:
Server:
image.png

Client:
image.png

基础API函数说明

1.寻址方式

因为Winsock 要兼容几个协议,所以必须使用通用的寻址方式。TCP/IP使用IP地址和端口号来指定一个地址,但是其他协议也许采用不同的形式。如果Winsock 强迫使用特定的寻址方式,添加其他协议就不大可能了。Winsock的第一个版本使用sockaddr结构来解决此问题
① sockaddr

struct sockaddr
{
	u_short	sa_family;
	char		sa_data[14];
};

在这个结构中,第一个成员sa_family 指定了这个地址使用的地址家族。sa_data成员存储的数据在不同的地址家族中可能不同。Winsock已经定义了sockaddr结构的TCP/IP版本——sockaddr_in结构。它们本质上是相同的结构,但是第2个更容易操作。
②sockaddr_in

struct sockaddr_in
{
	short		sin_family;			//地址家族
	u_short		sin_port;			//端口号
	struct		in_addr sin_addr;		//IP 地址
	char 			sin_zero[8];		//空字节,要设为0
}

此结构的最后8个字节没有使用,是为了与sockaddr结构大小相同才设置的。
sin_addr是IP地址(32位),它被定义为一个联合来处理整个32位的值,两个16位部分或者每个字节单独分开。描述32位IP地址的in_addr结构定义如下:
③ in_addr

struct in_addr{
	union{
		struct{ u_char s_b1,s_b2,s_b3,s_b4;} S_un_b;	//以4个u_char来描述
		struct{ u_short s_w1,s_w2;} S_un_w;		//以2个u_short来描述
		u_long S_addr;					//以1个u_long来描述
	}S_un;
}

用字符串“aa.bb.cc.dd”表示IP地址时,字符串中由点分开的4个域是以字符串的形式对in_addr结构中的4个u_char值得描述。由于每个字节的数值范围是0~255,所以各域的值是不可以超过255的。

2.字节顺序

字节顺序是长度跨越多个字节的数据被存储的顺序。例如,一个32位的长整型0x12345678跨越4个字节(每个字位8位)。Intel x86 机器使用小尾顺序(little-endian),意思是最不重要的字节首先存储。因此,数据0x12345678在内存中的存放顺序是0x78、0x56、0x34、0x12。大多数不使用小尾顺序的机器使用大尾顺序(big-endian),即最重要的字节首先存储。同样的值在内存中的存放顺序将是0x12、0x34、0x56、0x78。因为协议数据要在这些机器间传输,就必须选定其中的一种方式作为标准,否则会引起混淆。
TCP/IP协议统一规定使用大尾方式传输数据,也称为网络字节顺序。例如,端口号(它是一个16位的数字)12345(0x3039)的存储顺序是0x30、0x39。32位的IP地址也是以这种方式存储的,IP地址的每一部分存储在一个字节中,第一部分存储在第一个字节中。
上述sockaddr 和 sockaddr_in结构中,除了sin_family成员(它不是协议的一部分)外,其他所有值必须以网络字节顺序存储。Winsock提供了一些函数来处理本地机器的字节顺序如网络字节顺序的转换。

  • u_short htons(u_short hostshort); //转化一个u_short类型从主机字节顺序到TCP/IP网络字节顺序
  • u_long htonl(u_long hostlong); //转化一个u_long类型从主机字节顺序到TCP/IP网络字节顺序
  • u_short ntohs(u_short netshort); //转化一个u_short类型从TCP/IP网络字节顺序到主机字节顺序
  • u_long ntohl(u_long netlong); //转化一个u_long 类型从TCP/IP网络字节顺序到主机字节顺序

这些API是平台无关的,使用他们可以保证程序正确地运行在所有机器上。

3. 使用举例

在sockaddr_in结构中除了sin_family成员之外,所有的成员必须以网络字节顺序存储。下面是初始化sockaddr_in结构的例子

sockaddr_in sockAddr1,sockAddr2;
//设置地址家族
sockAddr1.sin_family = AF_INET;
//转化端口号80到网络字节顺序,并安排它到正确的成员
sockAddr1.sin_port = htons(80);
//inet_addr 函数转化一个“aa.bb.cc.dd”类型的IP地址字符串到长整型,
//它是以网络字节顺序记录的IP地址
//sin_addr.S_un.S_addr 指定了地址联合中的此长整型
sockAddr1.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
//通过设置4个字节部分,设置sockAddr2的地址
sockAddr2.sin_addr.S_un.S_un_b.s_b1 = 127;
sockAddr2.sin_addr.S_un.S_un_b.s_b2 = 0;
sockAddr2.sin_addr.S_un.S_un_b.s_b3 = 0;
sockAddr2.sin_addr.S_un.S_un_b.s_b4 = 1;

上例中inet_addr函数将一个由小数点分隔的十进制IP地址字符串转化成由32位二进制数表示的IP地址(网络自己顺序)。inet_ntoa是inet_addr函数的逆函数,它将一个网络字节顺序的32位IP地址转化成字符串。
char *inet_ntoa(struct in_addr in); //将32位的二进制数转化为字符串

4、Winsock库函数

所有的Winsock函数都是从WS2_32.dll库导入的,VC++在默认情况下并没有连接到该库,如果想使用Winsock API,就必须包含相应的库文件
#pragma comment(lib,"wsock32.lib")

① Winsock库的装入、初始化和释放

int WSAStartup(
	WORD wVersionRequested,	//应用程序支持的最高WinSock库版本。高字节为次版本号,低字节为主版本号
	LPWSADATA lpWSAData	//一个指向WSADATA结构的指针。它用来返回DLL库的详细信息
);

lpWSAData 参数用来取得DLL库的详细信息,结构定义如下:

typedef struct WSAData{
	WORD			wVersion;								//库文件建议应用程序使用的版本
	WORD			wHighVersion;							//库文件支持的最高版本
	char			szDescription[WSADESCRIPTION_LEN+1];	//库描述字符串
	char			szSystemStatus[WSASYS_STATUS_LEN+1];	//系统状态字符串
	unsigned short	iMaxSockets;							//同时支持的最大套接字的数量
	unsigned short	iMaxUdpDg;								//2.0版中已废弃的参数
	char FAR *		lpVendorInfo;							//2.0版中已废弃的参数
}WSADATA,FAR* LPWSADATA;

函数调用成功后返回0。否则要调用WSAGetLastError函数查看出错的原因。
int WSACleanup(void); //对应于一个WSAStartup(),释放Winsock库
② 套接字的创建和关闭

SOCKET socket(
	int af,	//用来指定套接字使用的地址格式,Winsock中支持AF_INET
	int type,	//用来指定套接字的类型
	int protocol	//配合type参数使用,用来指定使用的协议类型,可以是IPPROTO_TCP等
);

type参数用来指定套接字的类型。套接字有流式套接字(SOCK_STREAM)、数据报套接字(SOCK_DGRAM)和原始套接字(SOCK_RAW)。
当type参数指定为SOCK_STREAM和SOCK_DGRAM时,系统已经明确确定使用TCP和UDP协议来工作,所以protocol可以设置为0.
int closesocket(SOCKET s); //函数唯一的参数就是要关闭的套接字句柄
③ 绑定套接字到指定的IP地址和端口号

int bind(
	SOCKET s,			//套接字句柄
	const struct sockaddr *name,	//要关联的本地地址
	int namelen			//地址的长度

bind函数用在没有建立连接的套接字上,它的作用是绑定面向连接的或者无连接的套接字。当一个套接字被socket函数创建以后,他存在于指定的地址家族里,但是它是未命名的。bind函数通过安排一个本地名称到未命名的socket建立此socket的本地关联。本地名称包含3个部分:主机地址、协议号(分别为UDP或TCP)和端口号。
示例:

//填充sockaddr_in 结构
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(8888);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
//绑定这个套接字到一个本地地址
if(::bind(s,(LPSOCKADDR)&sin,sizeof(sin))==SOCKET_ERROR)
{
	printf("Failed bind() \n");
	::WSACleanup();
	return 0;
}

sockaddr_in 结构中的sin_familly字段用来指定地址家族,该字段和socket函数中的af参数的含义相同,所以唯一可以使用的值就是AF_INET。sin_port字段和sin_addr字段分别指定套接字需要绑定的端口号和IP地址。放入这两个字段的数据的字节顺序必须是网络字节顺序。由于网络字节顺序和Intel CPU的字节顺序刚好相反,所以必须首先用htons函数进行转换。
如果应用程序不关系所使用的地址,可以为互联网地址指定INADDR_ANY,为端口号指定0。如果互联网地址等于INADDR_ANY,系统会自动使用当前主机配置的所有IP地址,这简化了程序设计。
④ 设置套接字进入监听状态

int listen(
	SOCKET s,		//套接字句柄
	int backlog		//监听队列中允许保持的尚未处理的最大连接数量

⑤ 接收连接请求

SOCKET accept(
	SOCKET s,		//套接字句柄
	struct sockaddr* addr,	//一个指向sockaddr_in结构的指针,用于取得对方的地址信息
	int * addrlen		//是一个指向地址长度的指针

该函数在s上取出未处理链接中的第一个连接,然后为这个连接创建一个新的套接字,返回它的句柄。新创建的套接字是处理实际连接的套接字,它与s有相同的属性。
程序默认工作在阻塞模式下,这种方式下如果没有未处理的连接存在,accept函数会一直等待下去直到有新的连接发生才返回。
addrlen参数用于指定addr所指空间的大小,也用于返回地址的实际长度。如果addr活着addrlen是NULL,则没有关于远程地址的信息返回
⑥ 客户端连接

int connect(
	SOCKET s,		//套接字句柄
	const struct sockaddr FAR * name,	//一个指向sockaddr_in结构的指针,包含了要连接的服务器的地址信息
	int namelen		//sockaddr_in 结构的长度

第一个参数s是此连接使用的客户端套接字。另两个参数name和namelen用来寻址远程套接字(正在监听的服务器套接字)。
⑦ 收发数据

int send(
	SOCKET s,		//套接字句柄
	const char FAR* buf,	//要发送数据的缓冲区地址
	int len,		//缓冲区长度
	int flags		//指定了调用方式,通常设位0
);

int recv(SOCKET s,char FAR* buf,int len,int flags);

send函数在一个连接的套接字上发送缓冲区内的数据,返回发送数据的实际字节数。recv函数从对方接受数据,并存储它到指定的缓冲区。flags参数在这两函数中通常设为0。
在阻塞模式下,send将会阻塞线程的执行直到所有的数据发送完毕(或者一个错误发生),而recv函数将返回尽可能多的当前可用信息,一直到缓冲区指定的大小。

(゚д゚)σ弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌弌⊃