TCP/IP网络编程_1.3基于Windows平台的实现

1.3基于Windows平台的实现

Windows 套接字(以下简称Winsocket) 大部分是参考BSD系列UNIX套接字设计的, 所以很多地方都跟Linux 套接字类型. 因此, 只需更改Linux 环境下编写好的一部分网络编程内容, 就能在Windows 平台下运行. 本书也会同时讲解Linux 和 Windows两大平台, 这不会给大家增加负担, 反而会减轻负担.

同时学习Linux 和 Windows 的原因

大多数项目都在Linux 系列的操作系统下开发服务端, 而大多数客户端是在Windows平台下开发的. 不仅如此, 有时应用程序还需要两个平台之间相互切换. 因此, 学习套接字编程的过程中 , 有必要兼顾Windows 和 Linux 两大平台. 另外, 这两大平台下的套接字编程非常类似, 如果把其中相似的一部分放在一起讲解, 将大大提高学习效率. 这会不会增加学习负担? 一点也不. 只有理解好其中一个平台下的网络编程方法, 就很容易通过分析差异掌握另一平台.

为Windows 套接字编程设置头文件和库

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Winsock 的初始化

进行Winsock 编程是, 首先必须调用 WSAStarup 函数, 设置程序中用到的Winsock 版本, 并初始化相应版本的库.
在这里插入图片描述
有必要给出上述两个参数的详细说明. 先说第一个, Winsock 中存在多个版本, 应准备 WORD 类型的( WORD 是通过typedef 声明定义的 unsigned short 类型) 套接字版本信息, 并传递给该函数的第一个参数w VersionRequested. 若版本为1.2 则其中1 是主版本号, 2 是副版本号, 应传递0x0201.

如前所述, 高8位为副版本号, 低8位主版本号, 以此进行传递. 本书主要使用2.2版本, 古应传递0x0202. 不过, 以字节为单位手动构造版本信息有些麻烦, 借助MAKEWORD 宏函数则能轻松构建WORD 型版本信息.
在这里插入图片描述
接下来讲解第二个参数lpWSADATA, 次参数中需要传入 WSADATA 型结构体变量地址(LPWSADATA是WSADATA的指针类型). 调用完函数后, 相应参数中将填充已初始化的库信息. 虽无特殊含义, 但为了调用函数, 必须传递WSADATA 结构体变量地址. 下面给出WSAStartup 函数调用过程, 这段代码几乎已成为Winsock 编程的公式.
在这里插入图片描述
前面已经介绍了Winsock相关库的初始化方法, 接下来讲解如何注销该库–利用下面给出的函数.
在这里插入图片描述
调用该函数时, Winsock 相关库将归还Windows 操作系统, 无法再调用Winsock相关函数. 从原则上讲 , 无需再使用Winsock函数时才调用该函数, 但通常都在结束之前调用.

1.4 基于Windows 的套接字相关函数及示例

本节介绍的Winsock 函数与之前的Linux 套接字相关函数对应. 既然只是介绍, 就不做详细说明了, 目的只在于让各位体会基于Linux 和 Windows 的套接字函数之间的相似性.

基于 Windows 的套机字相关函数

首先介绍的函数与 Linux 下的 socket 含数提供相同功能. 稍后讲解返回值类型SOCKET.
在这里插入图片描述
下列函数与 Linux 的 bind函数相同, 调用其分配IP地址和端口号.
在这里插入图片描述
下列函数与Linux 的 listen 函数相同, 调用其使套接字可接收客户端的连接.
在这里插入图片描述
下列函数与 Linux 的 accept 函数相同, 调用其受理客户端连接请求.
在这里插入图片描述
下列函数与Linux 的 connect 函数相同, 调用其从客户端发送连接请求.
在这里插入图片描述
最后这个函数在关闭套机字时调用. Linux 中, 关闭文件可套接字是都会调用close 函数; 而Windows 中有专门用来关闭套接字的函数.
在这里插入图片描述
以上就是基于 Windows 的套接字相关函数, 虽然返回值和参数与 Linux 函数有所区别, 但具有相同功能的函数名是一样的, 正是这些特点时跨越两大操作系统平台的网络编程更加简单.

Windows 中的文件句柄和套接字句柄

Linux 内部也将套机字当做文件, 因此, 不管创建文件还是套机字 都返回文件描述符. 之前也通过实例介绍了文件描述符返回及编号的过程. Windosw 中通过调用系统函数创建文件时, 返回"句柄(handle)", 换言之, Windows 中的句柄相当于Linux 中的文件描述符. 只不过Windows 中要区分文件句柄和套接字句柄. 虽然都称"句柄", 但不想 Linux 那样完全一致. 文件句柄相关函数与套接字相关函数有区别的, 这一点不同于Linux 文件描述符.

既然对句柄有了一定理解, 接下来再观察基于Windows 的套接字相关函数, 这将加深各位对SOCKET 类型的参数和返回值的理解. 的确! 这就是为了保存套接字句柄整型值的新数据类型, 它由typedef 声明定义. 回顾socket , listen 和 accept 等套接字相关函数, 则更能体会与 Linux 中套接字相关函数的相似性.

有些程序员可能会问: “既然Winsock 是以UNIX, Linux系列的BSD套接字以原始设计的, 为什么不照搬过来, 而是存在一定差距呢?” 有人认为这是微软为了防止UNIX, Linux服务器端直接移植到 Windows 而故意为之. 从网络程序移植角度上看, 这也是可以理解的. 但我有不同意见. 从本质上说, 这两种操作系统内核结构上存在巨大差异, 而依赖于操作系统的代码实现风格也不尽相同, 连 Windows 程序员给变量命名的方式也不同于Linux 程序员. 从各方面考虑, 保存这种差异性就显得比较自然. 因此我个人认为, Windows 套接字与BSD系列的套接字编程方式有所不同是为了保持这种自然差异性.

创建基于 Windows 的服务器端和客户端

接下来将之前基于Linux 的服务器端与客户端实例转换到 Windows 平台. 目前想完全理解这些代码有些困难, 我们只需验证套接字相关函数的调用过程, 套接字库的初始化与注销过程即可.
先介绍服务器实例.

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	SOCKADDR_IN servAddr, clntAddr;

	int szClntAddr;
	char message[] = "Hello World!";
	if (argc != 2)
	{
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	/* 初始化套接字 */
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error!");
	}

	/* 创建套接字 */
	hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == INVALID_SOCKET)
	{
		ErrorHandling("socket() error!");
	}

	/* 给该套机字分配IP地址与端口号. */
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(atoi(argv[1]));

	if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ErrorHandling("bind() error!");
	}

	/* 调用listen函数使第28行创建的套机字成为服务器端套机字 */
	if (listen(hServSock, 5) == SOCKET_ERROR)
	{
		ErrorHandling("listen() error!");
	}

	szClntAddr = sizeof(clntAddr);
	/* 调用accept函数受理客户端连接请求 */
	hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
	if (hClntSock == INVALID_SOCKET)
	{
		ErrorHandling("accept() error");
	}

	/* 调用send函数向第53行连接的客户端传输数据 */
	send(hClntSock, message, sizeof(message), 0);

	closesocket(hClntSock);
	closesocket(hServSock);
	WSACleanup();

	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	char message[30];
	int strLen;
	if (argc != 3)
	{
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup((2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error!");
	}

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
	{
		ErrorHandling("socket() error!");
	}

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(argv[1]);
	servAddr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ErrorHandling("connect() error");
	}

	strLen = recv(hSocket, message, sizeof(message) - 1, 0);
	if (strLen == -1)
	{
		ErrorHandling("recv() error");
	}
	printf("Message from server: %s\n", message);

	closesocket(hSocket);
	WSACleanup();

	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

运行环境: vs 2019
运行结果:
在这里插入图片描述
在这里插入图片描述
shift + 右键 -> 有一个 : 在此处打开powershell窗口

基于 Windows 的I/O 函数

Linux 中套接字也是文件, 因而可以通过文件I/O函数read和write进行数据传输. 而Windows 中则不同. Windows 严格区分文件I/O 函数和套接字I/O函数. 下面介绍Winsock数据传输函数.
在这里插入图片描述

此函数与Linux 的write函数相比, 只是多出了最后的flags参数. 后续章节中将会给出该参数的详细说明, 在此之前需要注意, send 函数并非Windows 独有. Linux 中也有同样的函数, 它也是来自BSD 套接字. 只不过我在 Linux 相关实例中暂时只使用 read, write函数, 为了强调 Linux 环境下文件I/O 和 套接字I/O相同. 下面介绍与 send 函数对应的recv函数.
在这里插入图片描述
我只是在 Windows 环境下提前介绍了 send, recv 函数, 以后的 Linux 实例中也会涉及. 请不要误认为 Linux 中的read , write 函数就是对应于Windows 的send, recv 函数. 另外, 之前的程序代码中也给出了 send, recv 函数调用过程, 故不再另外给出相关实例.
在这里插入图片描述
1.5 习题
(1)套接字在网络编程的作用是什么? 为何称它为套接字?

P2,网络编程就是编写程序让两台联网的计算机相互交换数据。在我们不需要考虑物理连接的情况下,我们只需要考虑如何编写传输软件。操作系统提供了名为“套接字”,套接字是网络传输传输用的软件设备

socket英文原意是插座:我们把插头插到插座上就能从电网获得电力供给,同样,为了与远程计算机进行数据传输,需要连接到Internet,而变成中的“套接字”就是用来连接该网络的工具

(2)在服务器端创建套接字后,会依次调用listen函数和accept函数。请比较并说明两者作用
listen:将套接字转为可接受连接方式

accept:受理连接请求,并且在没有连接请求的情况调用该函数,不会返回。直到有连接请求为止。二者存在逻辑上的先后关系

(3)Linux中,对套接字数据进行I/O时可以直接使用I/O相关函数;而在Windows中则不可以。原因为何?
Linux把套接字也看作是文件,所以可以用文件I/O相关函数;而Windows要区分套接字和文件,所以设置了特殊的函数

(4)创建套接字后一般会给它分配地址,为什么?为了完成地址分配需要调用哪些函数?
要在网络上区分来自不同机器的套接字,所以需要地址信息。分配地址是通过bind()函数实现

(5)Linux中的文件描述符与Windows的句柄实际上非常类似。请以套接字为对象说明他们的含义。
Linux的文件描述符是为了区分指定文件而赋予文件的整数值(相当于编号)。Windows的文件描述符其实也是套接字的整数值,其目的也是区分指定套接字。

(6)底层文件I/O函数与ANSI标准定义的文件I/O函数之间有何区别?

ANSI标准定义的输入、输出函数是与操作系统(内核)无关的以C标准写成的函数。相反,底层文件I/O函数是直接提供的。理论上ANSI标准I/O提供了某些机制,性能上由于底层I/O

时间: 2020_05_22

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章