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

引言

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

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

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

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

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计时器。

在客户和服务器建立连接和断开连接的交互过程中,双方端点所经历的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;
}

通信截图如下:
在这里插入图片描述

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

L19002S

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值