【计算机网络】--- 流式套接字通讯

引言

流式套接字为网络应用程序提供了可靠的、面向链接的双向数据传输服务,实现了数据无差错、无重复的发送。它内设流量控制,被传输的数据看做是无记录便捷的字节流,在TCP/IP协议簇中,使用TCP协议来实现字节流传输,当用户想要发送大批量的数据或者对数据传输有较高要求的时候,就可使用流式套接字。固然,它适合于大多数应用场景,也是初学者使用套接字编程的主要方法。ios

TCP协议的传输特色(面试官常考点)

TCP协议是一个面链接的传输层协议,提供高可靠性字节流传输服务,主要用与一次传输要交换大量报文情形。
为了维护传输的的可靠性,TCP增长了许多开销,如:确认、流量控制、计时器以及链接管理等。程序员

端到端通讯:TCP提供给应用面向链接的接口。TCP链接时端到端的,客户应用程序在一端,服务器在另外一端。
创建可靠链接:TCP要求客户应用程序在与服务器交换数据前,先链接服务器,保证链接可靠创建,创建链接测试了网络的连通性。若是有故障发生,阻碍了分组到达远端系统,或者服务器不接受链接,那么企图链接就会失败,客户就会获得通知。
可靠交付:一旦创建链接,TCP保证数据将按发送时的顺序交付,没有丢失,也没有重复,若是由于故障而不能创建可靠交付,发送方会获得通知。
具备流控的传输:TCP控制数据传输的效率,防止发送数据的速率快与接收方的接收速率,所以TCP能够用于从快速计算机向慢速计算机传输数据。
双工传输:在任什么时候候,单个TCP链接都容许同时双向传送数据,并且不会相互影响,所以客户能够向服务器发送请求,而服务器能够经过同一个链接发送应答。
流模式:TCP从发送方向接收方发送没有报文边界的字节流。web

TCP的首部

TCP数据被封装在一个IP数据包中!!!以下图所示:
在这里插入图片描述
下图则显示了TCP首部的数据格式,若是不记选项字段,他们一般是20个字节
在这里插入图片描述面试

TCP首部个字段的含义以下(大体掌握)

  • 1.源、目的端口号:每一个TCP报文段都包含源端口号和目的端口号,用于寻找发送端和接收端的应用进程。
  • 2.序号和确认序号:序号用来表示从TCP发送端向TCP接收端发送的数据字节流,它表示在这个报文字段中的第一个数据字节。若是将字节流看做在两个应用程序间的单向流动,则TCP用序号对每一个字节流进行计数。序号是32位的无符号数。确认序号是发送确认的一端所指望收到的下一个序号。所以,确认序号应当是上次已成功收到数据字节序号加1。只有ACK标志为1时,确认序号字段才有效。
  • 3.首部长度:首部长度给出首部中32位字的数目。须要这个只是由于选项字段的长度是可变的。这个字段占4位,所以TCP最多有60字节的首部。若是没有选项字段,正常的长度时20字节。
  • 4.标志位:在TCP首部中有6个标志位。 它们中的多个可同时被设置为1,其含义分别以下:
信号 做用
URG 紧急指针是否有效
ACK 确认号是否有效
PSH 提示接收端应用程序马上从TCP缓冲区把数据读走
RST 对方要求从新创建链接; 咱们把携带RST标识的称为复位报文
SYN 请求创建链接; 咱们把携带SYN标识的称为同步报文段
FIN 通知对方, 本端要关闭了, 咱们称携带FIN标识的为结束报文段
  • 5.窗口大小:TCP的流量控制由链接的每一端经过声明的窗口大小来提供。窗口大小为字节数起始于确认序号字段指明的值,这个值是接收端正指望接受的字节编号。窗口大小是一个16位字段,于是窗口大小最大为65535字节。
  • 6.检验和:检验和覆盖了整个的TCP报文段,包含TCP首部、TCP伪首部和TCP数据。这是一个强制性的字段,必定是由发送端计算和存储的,并由接收端进行校验。
  • 7.紧急指针:只有当URG标志位置1时,紧急指针才有效。紧急指针是一个正的偏移量,与序号字段中的值相加表示紧急数据最后一个字节的序号。这是发送端向另外一端发送紧急数据的一种方式。
  • 8.选项:TCP首部的选项部分是TCP为了适应复杂网络环境和更好地服务应用层设计的,选项部分最长可达40字节。最多见的选项字段是最大报文段大小(MaximumSegment ,MSS)。每一个链接方一般都在通讯的第一个报文段(为创建链接而设置的SYN标志位的那个段)中指明这个选项。它指明本端所能接受的最大长度的报文段。
  • 9.数据:TCP报文段中的数据部分是可选的。例如在链接创建和链接终止时,双方交换的报文段仅有TCP首部。一方即便没有数据要发送,也使用没有任何数据的首部来确认收到的数据。在处理超时的许多状况中,也会发送不带任何数据的报文段。

TCP链接的创建和终止(面试官必考)

为了创建一条TCP链接,须要如下三个步骤来实现:编程

  • 1)请求端(一般称为客户)发送一个SYN报文段指明客户打算链接服务器端口号,以及初始序号(Initial Sequence
    Number,ISN),SYN请求发送后,客户进入SYN_SENT状态。
  • 2)服务器启动后首先进入LISTEN状态,当它接收到客户端发来的SYN请求后,进入SYN_RCV状态,发回包含服务器的初始序号的SYN报文段做为应答,同时将确认须要设置为客户的初始序号加1,对客户的SYN报文段进行确认。一个SYN将占用一个序号。
  • 3)客户接受到服务器的确认报文后进入ESTABLISHED状态,代表本方链接已经成功创建,客户将确认序号设置为服务器的ISN加1,的对服务器的SYN报文段进行确认,当服务器接收到该确认报文后,也进入ESTABLISHED状态。

“三次握手”。以下图所示

在这里插入图片描述
注意(面试官必考):通常由客户决定什么时候终止链接,由于客户进程一般由用户交互控制,例如Telnet的用户会键入quit命令来终止进程,既然一个TCP链接是双工的(即数据在两个方向上能同时传递),那么每一个方向必须单独关闭。终止一个链接要通过四次交互,当一方完成它的数据发送任务以后,发送一个FIN报文段来终止这个方向的链接。当一段收到FIN,他必须通知应用层另外一端已经终止了那个方向的数据传输。发送FIN报文段一般是应用层进行关闭的结果。下图显示了“四次挥手”的过程:服务器

  • 1)客户的应用进程主动发起关闭链接请求,它将致使TCP客户发送一个FIN报文段,用来关闭从客户到服务器的数据传发送,此时客户进入FIN_WAIT_1状态。

在这里插入图片描述

  • 2)当服务器收到这个FIN,它发挥一个ACK,进入CLOSE_WAIT状态,确认序号为收到这个序号加1,与SYN同样,一个FIN将占用一个序号。客户收到该确认后进入FIN_WAIT_状态,表面本方链接已经关闭,担任能够接受到服务器发来的数据。
  • 3)接着服务器程序变比本方链接,其TCP端发送一个FIN报文段,进入LAST_ACK状态,当客户接收到该报文后进入TIME_WAIT状态。
  • 4)客户在收到服务器发来的FIN请求后,发回一个确认,并将确认序号设置为收到的序号加1,发送FIN将致使应用程序关闭他们的链接,服务器接收到该确认后,链接关闭。这些FIN的ACK是由TCP软件自动产生的。

注意(常考点)

在该链接关闭过程当中咱们发现,当四次挥手完成后,客户并无直接关闭链接,而是进入TIME_WAIT状态,且此状态会保留两个最大段生存时间(2MSL),等待2MSL时间以后,客户也关闭链接并释放它的资源。网络

为何须要TIME WAIT状态呢?设立TIME WAIT有两个目的:并发

  • 1)当由主动关闭方发送的最后的ACK丢失并致使另方从新发送FIN时,TIME_WAIT维护链接状态。当最后的ACK发生丢失时,因为执行被动关闭的方没有接收到最后序号的ACK,则会运行超时并从新传输FIN。假如执行主动关闭的一方不进人TIME_WAIT 状态就关闭了链接,那么此时重传的FIN到达时,因为TCP已经再也不有链接的信息了,因此它就用RST (重置链接)报文段应答,致使对等方进人错误状态而不是有序终止状态。由此看来,TIME_WAIT状态延长了TCP对当前链接的维护信息,对于正确处理链接的正常关闭过程当中确认报文丢失是颇有必要的。
  • 2) TIME WAIT为链接中“离群的段”提供从网络中消失的时间。IP 数据包在广“域网传输中不只可能会丢失,还可能延迟。若是延迟或重传报文段在链接关闭以后到达,一般状况下,由于TCP仅仅丢弃该数据并响应RST,当该报文段到达发出延时报文段的主机时,由于该主机也没有记录该链接的任何信息,因此它也丢弃该报文段。然而若是两个相同主机之间又创建了一个具备相同端口号的新链接,那么离群的段就可能被当作是属于新链接的,若是离群的段中数据的任何序号刚好处在新链接的当前接收窗口中,数据就会被新链接接收,其结果是破坏新链接,使TCP不能保证以顺序的方式递交数据。所以TIME_WAIT状态确保了旧链接的报文段在网络上消失以前不会被重用,从而防止其在上述状况下扰乱新链接。

一般状况下,仅有主动关闭链接的一方会进人 TIME WAIT状态。RFC793 中定义MSL为2分钟,在这个定义下,链接在TIME WAIT状态下保持4分钟,而实际中,MSL的值在不一样的TCP协议实现中的定义并不相同。若是链接处于TIME WAIT状态期间有报文段到达,则从新启动一个2MSL计时器。socket

在客户和服务器创建链接和断开链接的交互过程当中,双方端点所经历的TCP状态发生了次特物为发生网络环境异常时,这些状态的变迁有助于理解和解释基于流式套接字的应用程序在运行中的表现。tcp

流式套接字编程的适用场合

方式套接字基于可靠的数据流传输服务,这种服务的特色是面向链接、可靠。面向链接占决定了 流式套接字的传输代价大,且只适合于一对的数据传输;而可靠的特 点意味下层应用程序在设计开发时不须要过多地考虑数据传输过程当中的丢失、乱序、重复问题。总结来看,流式套接字适合在如下场合使用:

1大数据量的数据传输应用。流式套接字适合文件传输这类大数据量传输的应用,传输的内容能够是任意大的数据,其类型能够是ASCII文本,也能够是二进制文件。在这种应用数据传输量大,对数据传输的可靠性要求比较高,且与数据传输的代价相比,链接场景下,维护的代价微乎其微。

2)可靠性要求高的传输应用。流式套接字适合应用在可靠性要求高的传输应用中,在这种状况下,可靠性是传输过程首先要知足的要求,若是应用程序选择使用UDP协议或其余不可靠的传输服务承载数据,那么为了不数据丢失、乱序、重复等问题,程序员必需要考虑以上诸多问题带来的应用程序的错误,由此带来复杂的编码代价。

流式套接字的通讯过程

流式套接字的网络通讯过程是在链接成功创建的基础上完成的。
(1)基于流式套接字的服务器进程的通讯过程在通讯过程当中,服务器进程做为服务提供方,被动接受链接请求,决定接受或拒绝该请求,并在已创建好的链接上完成数据通讯。其基本通讯过程以下:
1 ) Windows Sockets DLL初始化,协商版本号;
2)建立套接字,指定使用TCP (可靠的传输服务)进行通讯;
3)指定本地地址和通讯端口;
4)等待客户的链接请求;
5)进行数据传输;
6)关闭套接字;
7)结束对Windows Sockets DLL的使用,释放资源。

(2)基于流式套接字的客户进程的通讯过程
在通讯过程当中,客户进程做为服务请求方,主动请求创建链接,等待服务器的链接确在已创建好的链接上完成数据通讯。其基本通讯过程以下:
1 ) Windows Sockets DLL初始化,协商版本号;
2)建立套接字,指定使用TCP (可靠的传输服务)进行通讯;
3)指定服务器地址和通讯端口;
4)向服务器发送链接请求;
5)进行数据传输;
6)关闭套接字;
7)结束对Windows Sockets DLL的使用,释放资源。

通讯代码以下

客户端:

#define _CRT_SECURE_NO_WARNINGS 1

// ShuruxinxClient.cpp : 客户端程序,用户能够从键盘输入信息并发送给服务器。
//

#include<iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <WS2tcpip.h>
#include<string.h>
#pragma comment (lib,"ws2_32.lib")
#pragma warning(disable :4996)
#define SERVER_PORT "8888"
#define BUFFER_LEN 512
using namespace std;

#define SERVER_PORT "8888"
#define BUFFER_LEN 512

int main(int argc, char * argv[])
{ 
 
  
	struct addrinfo* result = NULL, *ptr = NULL, hints;
	WSADATA wsaData;
	SOCKET ConnectSocket;
	char sendbuf[BUFFER_LEN];
	char recvbuf[BUFFER_LEN];
	int iResult;

	if (argc != 1) { 
 
  
		printf("Usage: %s server ip address\n", argv[0]);
		return 1;
	}

	iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != 0) { 
 
  
		printf("WSAStartup failed with error: %d\n", iResult);
		return 1;
	}

	ZeroMemory(&hints, sizeof(hints));
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;

	iResult = getaddrinfo(NULL, SERVER_PORT, &hints, &result);//将输入参数argv[1]中指定的服务器信息写入result
	if (iResult != 0) { 
 
  
		printf("getaddrinfo failed with error: %d\n", iResult);
		WSACleanup();
		return 1;
	}

	ConnectSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);//使用result指定的信息建立套接字
	if (ConnectSocket == INVALID_SOCKET) { 
 
  
		printf("socket failed with error: %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	iResult = connect(ConnectSocket, result->ai_addr, result->ai_addrlen);//使用套接字ConnectSocket向result中指定的服务器请求链接
	if (iResult == SOCKET_ERROR) { 
 
  
		printf("connect failed with error: %ld\n", iResult);
		closesocket(ConnectSocket);
		WSACleanup();
		return 1;
	}
	freeaddrinfo(result);//释放动态分配的地址信息结构体result

	while (gets_s(sendbuf) != NULL) { 
 
  //从键盘获取输入字符串 
		if (*sendbuf == 'Q') { 
 
  
			closesocket(ConnectSocket);
			return 0;
		}
		iResult = send(ConnectSocket, sendbuf, strlen(sendbuf), 0);
		if (iResult == SOCKET_ERROR) { 
 
  
			printf("send failed with error: %d\n", WSAGetLastError());
			closesocket(ConnectSocket);
			WSACleanup();
			return 1;
		}
		do { 
 
  
			memset(recvbuf, 0, BUFFER_LEN * sizeof(char));
			iResult = recv(ConnectSocket, recvbuf, strlen(recvbuf), 0);
			if (iResult > 0)
			{ 
 
  
				printf("Received message from client: %d\n", iResult);
			}
			else if (iResult == 0)
			{ 
 
  
				printf("请继续输入要发送数据:");
			}
			else { 
 
  
				printf("recv failed with error:%d\n", WSAGetLastError());
			}
		} while (iResult > 0);
	}
	closesocket(ConnectSocket);
	WSACleanup();
	return 0;
}

服务器:

// DuokehuServer.cpp : 为多客户提供服务的服务器端程序。
//
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <WS2tcpip.h>
#include<string.h>
#pragma comment (lib,"ws2_32.lib")
#pragma warning(disable :4996)
#define SERVER_PORT "8888"
#define BUFFER_LEN 512
using namespace std;

int main(int argc, char * argv[])
{ 
 
  
	WSADATA wsaData;
	SOCKET ListenSocket = INVALID_SOCKET;
	SOCKET ClientSocket = INVALID_SOCKET;
	struct addrinfo hints, *result = NULL;
	struct sockaddr_in clientaddr;
	char sendbuf[BUFFER_LEN];
	char recvbuf[BUFFER_LEN];
	int iResult, isendResult;

	memset(recvbuf, 0, BUFFER_LEN * sizeof(char));
	iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != 0)
	{ 
 
  
		printf("WSAStartup failed with error: %d\n", iResult);
		return 1;
	}
	ZeroMemory(&hints, sizeof(hints));
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;
	hints.ai_flags = AI_PASSIVE;
	iResult = getaddrinfo(NULL, SERVER_PORT, &hints, &result);
	if (iResult != 0) { 
 
  
		printf("getaddrinfo failed with error %d\n", iResult);
		WSACleanup();
		return 1;
	}

	ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
	if (ListenSocket == INVALID_SOCKET) { 
 
  
		printf("socket failed with error %d\n", WSAGetLastError());
		freeaddrinfo(result);
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
	if (iResult == SOCKET_ERROR) { 
 
  
		printf("bind failed with error %d\n", WSAGetLastError());
		freeaddrinfo(result);
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}
	freeaddrinfo(result);

	iResult = listen(ListenSocket, SOMAXCONN);
	if (iResult == SOCKET_ERROR) { 
 
  
		printf("listen failed with error %d\n", WSAGetLastError());
		freeaddrinfo(result);
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	for (;;) { 
 
  
		int addrlenth = sizeof(clientaddr);
		ClientSocket = accept(ListenSocket, (struct sockaddr*)& clientaddr, &addrlenth);
		if (iResult == INVALID_SOCKET) { 
 
  
			printf("accept failed with error %d\n", WSAGetLastError());
			closesocket(ListenSocket);
			WSACleanup();
			return 1;
		}

		char* peeraddr = inet_ntoa(clientaddr.sin_addr);

		do { 
 
  
			iResult = recv(ClientSocket, recvbuf, BUFFER_LEN, 0);
			if (iResult > 0) { 
 
  
				printf("接收客户端的消息: %s\n", recvbuf);
				ZeroMemory(&recvbuf, sizeof(hints));
				isendResult = send(ClientSocket, sendbuf, strlen(sendbuf), 0);
				if (isendResult == SOCKET_ERROR) { 
 
  
					printf("send failed with error %d\n", WSAGetLastError());
					closesocket(ClientSocket);
					WSACleanup();
					break;
				}
				printf("接收成功\n");
			}
			else if (iResult == 0) { 
 
  
				printf("Connection closing...\n");
				iResult = shutdown(ClientSocket, SD_SEND);
				if (iResult == SOCKET_ERROR) { 
 
  
					printf("shutdown failed with error %d\n", WSAGetLastError());
					closesocket(ClientSocket);
					WSACleanup();
					break;
				}
				closesocket(ClientSocket);
				WSACleanup();
				break;
			}
			else { 
 
  
				printf("recv failed with error:%d\n", WSAGetLastError());
				iResult = shutdown(ClientSocket, SD_SEND);
				if (iResult == SOCKET_ERROR) { 
 
  
					printf("shutdown failed with error %d\n", WSAGetLastError());
					closesocket(ClientSocket);
					WSACleanup();
					break;
				}
			}

		} while (iResult > 0);

	}

	closesocket(ListenSocket);
	WSACleanup();
	return 0;
}

通讯截图以下:
在这里插入图片描述