流式套接字为网络应用程序提供了可靠的、面向连接的双向数据传输服务,实现了数据无差错、无重复的发送。它内设流量控制,被传输的数据看作是无记录便捷的字节流,在TCP/IP协议簇中,使用TCP协议来实现字节流传输,当用户想要发送大批量的数据或者对数据传输有较高要求的时候,就可以使用流式套接字。当然,它适合于大多数应用场景,也是初学者使用套接字编程的主要方法。
TCP协议是一个面连接的传输层协议,提供高可靠性字节流传输服务,主要用与一次传输要交换大量报文情形。
为了维护传输的的可靠性,TCP增加了许多开销,如:确认、流量控制、计时器以及连接管理等。
端到端通信:TCP提供给应用面向连接的接口。TCP连接时端到端的,客户应用程序在一端,服务器在另一端。
建立可靠连接:TCP要求客户应用程序在与服务器交换数据前,先连接服务器,保证连接可靠建立,建立连接测试了网络的连通性。如果有故障发生,阻碍了分组到达远端系统,或者服务器不接受连接,那么企图连接就会失败,客户就会得到通知。
可靠交付:一旦建立连接,TCP保证数据将按发送时的顺序交付,没有丢失,也没有重复,如果因为故障而不能建立可靠交付,发送方会得到通知。
具有流控的传输:TCP控制数据传输的效率,防止发送数据的速率快与接收方的接收速率,因此TCP可以用于从快速计算机向慢速计算机传输数据。
双工传输:在任何时候,单个TCP连接都允许同时双向传送数据,而且不会相互影响,因此客户可以向服务器发送请求,而服务器可以通过同一个连接发送应答。
流模式:TCP从发送方向接收方发送没有报文边界的字节流。
TCP数据被封装在一个IP数据包中!!!如下图所示:
下图则显示了TCP首部的数据格式,如果不记选项字段,他们通常是20个字节。
信号 | 作用 |
---|---|
URG | 紧急指针是否有效 |
ACK | 确认号是否有效 |
PSH | 提示接收端应用程序立刻从TCP缓冲区把数据读走 |
RST | 对方要求重新建立连接; 我们把携带RST标识的称为复位报文 |
SYN | 请求建立连接; 我们把携带SYN标识的称为同步报文段 |
FIN | 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段 |
为了建立一条TCP连接,需要以下三个步骤来实现:
注意(面试官必考):一般由客户决定何时终止连接,因为客户进程通常由用户交互控制,例如Telnet的用户会键入quit命令来终止进程,既然一个TCP连接是双工的(即数据在两个方向上能同时传递),那么每个方向必须单独关闭。终止一个连接要经过四次交互,当一方完成它的数据发送任务之后,发送一个FIN报文段来终止这个方向的连接。当一段收到FIN,他必须通知应用层另一端已经终止了那个方向的数据传输。发送FIN报文段通常是应用层进行关闭的结果。下图显示了“四次挥手”的过程:
在该连接关闭过程中我们发现,当四次挥手完成后,客户并没有直接关闭连接,而是进入TIME_WAIT状态,且此状态会保留两个最大段生存时间(2MSL),等待2MSL时间之后,客户也关闭连接并释放它的资源。
为什么需要TIME WAIT状态呢?设立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
#include
#include
#include
#include
#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
#include
#include
#include
#include
#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;
}