TCP/IP网络编程(4)

TCP套接字的半关闭

TCP的断开过程比建立连接的过程更加的重要,一般在建立连接的过程中不会出现大的问题,但是在断开连接的过程中,可能发生预想不到的情况。

Linux中的close函数和windows下的closesocket函数意味着完全断开连接,既不能传输数据,也不能接收数据,因此在某些条件下,直接调用这两个函数显得不够优雅。如下图所示:

假设主机A与主机B在进行通讯,主机A发送完最后的数据,调用close函数断开连接,之后主机A再无法收到数据B传输的数据,因此,由主机B传输的,主机A必须接收的数据就被销毁了。套接字为我们提供了一种半关闭的方法(Half-Close),半关闭是指只保留接收功能,或者只保留发送功能,而断开另一部分功能。

两台主机通过建立连接后,进入可交换数据的状态,称为流的形成状态。这里的流可理解为数据流,数据流具有流动方向。因此为了实现双向通信,套接字需要建立两个流(如上图所示)。

直接调用close或者closesocket会将两个流同时断开。

半关闭的函数:
 

// Linux

#include <sys/socket.h>

int shutdown(int sock, int howto);

sock: 需要断开的套接字文件描述符

howto:断开方式 

howto参数的可选值为:

  1. SHUT_RD 断开输入流
  2. SHUT_WR 断开输出流
  3. SHUT_RDWR  同时断开I/O流

若套接字断开输入流,则套接字无法接受数据,即使缓冲区收到数据也会抹去,而且无法调用输入相关函数。如果中断输出流,则无法向外传输数据,但是如果此时输出缓冲区中还有未传递的数据,则会将缓冲区中的数据传输到目标主机。

半关闭套接字的应用场景

假设客户端需要向服务器请求文件,服务器在向客户端传输文件数据的时候,需要告诉客户端,何时文件传输结束。如果传输文件的服务器只是不断的向客户端传输数据,客户端则无法知道需要接收到数据到何时。客户端不能无休止的调用读取数据函数(输入函数),因为这可能造成客户端程序阻塞(调用的函数未返回)。

一种解决方案是让服务端与客户端约定一个表示文件结尾的字符,但是这种方法也有问题,如果文件中有与约定的字符相同的内容,则会导致文件传输失败,即会导致文件传输提前结束(此处应该可以通过文件长度或者校验码进行校验)。

还有一种解决此问题的方法:服务端向客户端传递EOF表示文件传输结束,客户端会通过输入函数的返回值判断接收到EOF,这样就避免了文件结束符与文件内容冲突。

关键问题:服务端何时发送EOF?  断开输出流时向客户端传递EOF

如果服务端调用close函数同时关闭IO流,虽然也会向对方发送EOF,但是此时服务端再无法收到客户端的回复信息,如果调用shutdown函数,只关闭服务端的输出流,这样既可以向客户端发送EOF,又能接收到客户端的回复信息。

基于半关闭套接字的文件传输服务器实现

服务端设计:

// fileServer.cpp : 定义控制台应用程序的入口点。
//

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

#pragma comment(lib, "Ws2_32.lib")

#define  BUFF_SIZE  10        // 定义缓冲区的大小
#define  PORT       13400          // 定义通讯端口


// error handler
void error_handle(char* message)
{
	printf("%s\n", message);
	system("pause");
	exit(1);
}


int main(int argc, char* argv[])
{
	WSADATA wsadata;
	SOCKET serverSock, clientSock;    
	
	FILE* fp;                         // 文件指针

	char buffer[BUFF_SIZE];           // 定义缓冲区

	SOCKADDR_IN serverAddr, clientAddr;

	int addrSize = sizeof(clientAddr);

	if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
	{
		error_handle("Failed to init socket lib.");
	}

	// open file
	fp = fopen("data.txt", "rb");        // include string.h

	serverSock = socket(PF_INET, SOCK_STREAM, 0);

	// 初始化服务端地址
	memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddr.sin_port = htons(PORT);

	if (bind(serverSock, (SOCKADDR*)&serverAddr, addrSize) == SOCKET_ERROR)
	{
		error_handle("Failed to bind server socket.");
	}

	if (listen(serverSock, 5) == SOCKET_ERROR)
	{
		error_handle("Filed to listen the client socket!");
	}

	while (true)
	{
		clientSock = accept(serverSock, (SOCKADDR*)&clientAddr, &addrSize);

		if (clientSock == INVALID_SOCKET)
			continue;

		// 接收客户端的连接
		printf("Successfully accept connect from host: %s %d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

		memset(buffer, 0, BUFF_SIZE);

		// 读取文件
		int readCount = fread((void*)buffer, BUFF_SIZE, 1, fp);     // 

		while (readCount > 0)
		{
			// 将读取的文件内容发送到客户端
			send(clientSock, buffer, BUFF_SIZE, 0);

			memset(buffer, 0, BUFF_SIZE);

			readCount = fread((void*)buffer, BUFF_SIZE, 1, fp);
		}

		// 发送结束  关闭服务端的输出流,向客户端传递EOF
		shutdown(clientSock, SD_SEND);        // SD_RECEIVE;

		// 接收服务端的回复信息
		memset(buffer, 0, BUFF_SIZE);
		recv(clientSock, buffer, BUFF_SIZE, 0);

		const char ackMsg[] = "OK!";
		if (strcmp(buffer, ackMsg) == 0)
		{
			// 正确收到服务端的回复信息
			printf("Client has successfully received file from Server.\n");
		}
		else
		{
			printf("Client failed to receive file from server!\n");
		}

		closesocket(clientSock);
	}

	fclose(fp);

	WSACleanup();

	system("pause");

    return 0;
}

客户端设计:

// fileClient.cpp : 定义控制台应用程序的入口点。
//

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

#pragma comment(lib, "Ws2_32.lib")

#define  BUFF_SIZE        10         // 定义缓冲区的大小
#define  PORT             13400          // 定义通讯端口
#define  SERVER_ADDRESS   "127.0.0.1"
#define  FILE_NAME        "recv.dat"


// error handler
void error_handle(char* message)
{
	printf("%s\n", message);
	system("pause");
	exit(1);
}


int main(int argc, char* argv[])
{
	WSAData wsadata;

	char buffer[BUFF_SIZE];
	memset(buffer, 0, BUFF_SIZE);

	// 初始化套接字库
	if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
	{
		error_handle("Failed to init socket lib!");
	}

	FILE* fp;

	SOCKET serverSocket;           // 存储端套接字
	SOCKADDR_IN serverAddr;        // 存储服务端地址

	serverSocket = socket(PF_INET, SOCK_STREAM, 0);

	if (serverSocket == INVALID_SOCKET)
	{
		error_handle("Falied to init client socket");
	}

	// 初始化服务端地址
	memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family      = AF_INET;
	serverAddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
	serverAddr.sin_port        = htons(PORT);

	// 连接客户端
	if (connect(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
	{
		error_handle("Failed to connect to Server!");
	}

	printf("Successfully connected to Server!\n");

	// 连接服务端成功, 打开文件准备写入数据
	fp = fopen(FILE_NAME, "wb");
	int readCount;

	while ((readCount = recv(serverSocket, buffer, BUFF_SIZE, 0)) != 0)
	{
		fwrite(buffer, readCount, 1, fp);
		memset(buffer, 0, BUFF_SIZE);
	}

	printf("Finished receive data from Server!\n");

	char* msg = "OK!";

	send(serverSocket, msg, strlen(msg), 0);

	fclose(fp);

	closesocket(serverSocket);

	WSACleanup();

    return 0;
}

服务端运行结果:

 客户端运行结果:

 域名和网络地址

域名系统(DNS)是对域名和IP地址进行转化的系统。为了解决IP地址难于记忆和表述,将原本的服务器的IP地址,用域名进行取代。为不同的IP地址分配相应得分域名

例如,在访问百度的时候,输入www.baidu.com,而不是直接输入IP地址,域名实际上是为服务器分配的虚拟地址,而非实际地址,为了实现在输入域名的时候,能够正常访问网站,需要将域名转换为实际的IP地址,而DNS服务器就承担着这种角色。在所有计算机中都保存着默认DNS服务器地址,可以通过这个默认的DNS服务器获取到相应的域名对象的IP地址。

在实际使用中,一般不会去修改域名,但是IP地址有可能会发生变化。可以通过ping命令查看域名对应的IP地址,ping命令用于验证IP数据报是否到达目的地,在这个过程中会经历从域名到IP的转换过程。

可以通过nslookup命令查看计算机中默认的DNS服务器地址。

计算中默认的DNS服务器并不知到网络上所有的域名的IP地址信息,若该DNS服务器无法解析域名,此时它会去询问其他的DNS服务器,并提供给用户。默认的DNS服务器收到自己无法解析的请求时,会向上级DNS服务器进行询问,通过这种方式逐级向上传递信息,到达顶级DNS服务器,即根DNS服务器,它知到该向哪个DNS服务器询问,向下级DNS服务器传递解析请求,得到IP地址后原路返回,最后将解析得到的IP地址传递到发起请求的主机。DNS本质上是一种层次化管理的分布式数据库系统。

IP地址与域名之间的转换

假设在客户端程序中,需要访问某个服务器,如果将IP地址以及端口号直接写在客户端程序中,会导致如果服务器的IP地址或者端口号发生变化,则导致此时的客户端程序失效,此时不能总要求用户卸载现在的客户端,而安装新版本的客户端,相比之下,服务端的域名变更的频率远远低于IP地址的变更频率。因此,在程序中,用域名去获取对应的IP地址,然后再进行访问,这种方式更加好。

1. 利用域名获取IP地址

通过如下方法实现域名到IP地址的转换:(Linux下)

#include <netdb.h>

struct hostent* gethostbyname(const char* name);
struct hostent
{
	char* h_names;            // official name
	char** haliases;          // alias list
	int h_addrType;           // host address type
	int h_length;             // address length
	char** h_addr_list;       // address list
};

返回的信息中包含多个字段:

h_name: 该变量中存有官方域名,官方域名代表某一主页

h_aliases: 可以通过多个域名访问同一个主页。同一个IP可以绑定多个域名,这些信息可以通过这个变量获得。

h_addrType: 支持IPV以及IPV6地址, 如果是IPV4, 则值为AF_INET

h_length: 存储IP地址的长度,IPV4为4字节,因此值为4,IPV6是16字节,因此值为16

h_addr_list: 通过此变量以整数的形式保存域名所对应的IP地址。用户较多的网站可能会分配多个IP地址给同一个域名,利用多个服务器实现负载均衡,此时依然可以通过此变量获取域名对应的IP地址列表信息。

// demonTest.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>

#pragma comment(lib, "Ws2_32.lib")

// error handler
void error_handle(char* message)
{
	printf("%s\n", message);
	system("pause");
	exit(1);
}

int main()
{
	WSAData wsadata;

	struct hostent *host;

	const char* domainName = "www.baidu.com";

	if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
	{
		error_handle("Failed to init socket library");
	}

	host = gethostbyname(domainName);

	// 获取官方域名
	printf("Official Name: %s\n", host->h_name);

	printf("The Aliases list: \n");
	for (int i=0; host->h_aliases[i]; i++)
	{
		printf("The alias %d is %s\n", i + 1, host->h_aliases[i]);
	}

	// 地址类型
	char* addrType = host->h_addrtype == AF_INET ? "IPV4" : "IPV6";

	printf("The address type is %s\n", addrType);

	// 获取域名对应的所有IP地址
	for (int i=0; host->h_addr_list[i]; i++)
	{
		struct in_addr* address = (struct in_addr*)(host->h_addr_list[i]);
		char* ipStr = inet_ntoa(*address);
		printf("IP address is %s\n", ipStr);
	}

	system("pause");

    WSACleanup();

    return 0;
}

stdafx.h中请添加:

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS

运行结果:

2. 利用IP地址获取域名

#include <netdb.h>

struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);
 

addr:含有IP地址信息的In_addr结构体,为了兼容IPV6,传入时候需要转成char*

len:IP地址的长度,IPV4 为4, IPV6为16

family:地址族信息, AF_INET, AF_INET6

// demonTest.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>

#pragma comment(lib, "Ws2_32.lib")

// error handler
void error_handle(char* message)
{
	printf("%s\n", message);
	system("pause");
	exit(1);
}

int main()
{
	WSAData wsadata;

	struct hostent *host;

	SOCKADDR_IN addr;

	char* IP = "117.177.216.32";     // www.163.com

	if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
	{
		error_handle("Failed to init socket library");
	}

	memset(&addr, 0, sizeof(addr));
	addr.sin_addr.s_addr = inet_addr(IP);

	host = gethostbyaddr((char*)&addr.sin_addr, 4, AF_INET);

	if (!host)
	{
		error_handle("Error occurs while getting host");
	}

	// 获取官方域名
	printf("Official Name: %s\n", host->h_name);

	printf("The Aliases list: \n");
	for (int i=0; host->h_aliases[i]; i++)
	{
		printf("The alias %d is %s\n", i + 1, host->h_aliases[i]);
	}

	// 获取域名对应的所有IP地址
	for (int i=0; host->h_addr_list[i]; i++)
	{
		struct in_addr* address = (struct in_addr*)(host->h_addr_list[i]);
		char* ipStr = inet_ntoa(*address);
		printf("IP address is %s\n", ipStr);
	}

	system("pause");

	WSACleanup();

    return 0;
}

// to be continued...

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值