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参数的可选值为:
- SHUT_RD 断开输入流
- SHUT_WR 断开输出流
- 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...
?
|