《TCP/IP网络编程》学习笔记 | Chapter 4:基于TCP的服务器端/客户端(2)
《TCP/IP网络编程》学习笔记 | Chapter 4:基于TCP的服务器端/客户端(2)回声客户端的完美实现
在上一篇,我们发现并提出了对于回声客户端的缺陷。在这里,让我们一起来尝试解决这个问题。
回声客户端的问题
先回顾一下回声服务器端的I/O相干代码。
服务器端:
while((str_len = read(clnt_sock , message , BUF_SIZE)) != 0)
write(clnt_sock , message , str_len);
客户端:
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message , BUF_ SIZE , stdin);
write(sock , message , strlen(message));
str_len=read(sock , message , BUF_SIZE - 1);
message = 0;
printf(" message from server: %s", message );
}
两者都在循环中调用read和write函数。而在回声客户端中传输字符串时,调用write函数将字符串一次性发送,在没有考虑大概的处置惩罚时延和传输时延情况下,只调用了一次read函数,意在读取发送出去的完整字符串,这就是问题所在。
那我们是否可以给read函数前参加一个耽误函数,过一段时间再去接收数据呢?
这是一种方法,但是耽误应该控制在多少?很显然,这个是不好把握的。我们应当尝试从“提前确认接收数据的巨细”这个方向来入手。
回声客户端问题的解决方法
对接收到的字符串长度做记录,循环调用read函数,当接收到的字符串长度大于发送时的长度,停止循环,竣事read函数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message;
int str_len, recv_len, recv_cnt;
struct sockaddr_in serv_adr;
if (argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv);
serv_adr.sin_port = htons(atoi(argv));
if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("connect() error");
else
puts("Connected..........");
while (1)
{
fputs("Input message(Q to quit):", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
str_len = write(sock, message, strlen(message));
recv_len = 0;
while (recv_len < str_len)
{
recv_cnt = read(sock, &message, BUF_SIZE - 1);
if (recv_cnt == -1)
error_handling("read() error");
recv_len += recv_cnt;
}
message = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
为何在循环中 recv_len<str_len 作为条件,而不消 recv_len != str_len呢?
由于存在一种大概——当接收到的字符串长度在某种条件下大于原来发出的字符串长度时,整个语句块将陷入死循环,反复调用read函数。
假如问题不在于回声客户端:定义应用层协议
若不能再用字符长度来界定的情况下,又该怎样解决这类数据边界的问题呢?
答案在于——去定义应用层协议。在之前的回声服务器端/客户端中我们就定义过如下协议:“收到Q就立刻终止连接”。同样,收发数据过程中也需要定好规则以表现数据的边界,又或提前告知收发数据的巨细。服务器端/客户端实现过程中逐步定义的这些规则集合就是应用层协议。
可以看出,应用层协议并不是高深莫测,只不过是为特定程序的实现而制定的规则。
下面编写一个示例程序以体验应用层协议的定义过程。该程序中,服务器端从客户端获得多个数字和运算符信息。服务器端收到数字后对其进行加减乘运算,然后把计算效果传回客户端。例如,向服务器端传递3、5、9的同时哀求加法运算,则客户端收到 3+5+9 的运算效果;若哀求做乘法运算,则客户端收到 3×5×9 的运算效果。而假如向服务器传递4、3、2 的同时要求做减法,则客户端将收到 4-3-2 的运算效果,即第一个参数成为被减数。
计算器服务器端/客户端示例
在编写程序之前,我们需要先设计一下应用层协议。为了简单起见,我们只设计了最低标准的协议,在实际的应用程序实现中需要的协议更具体、更正确。应用层协议规则定义如下:
[*]客户端连接到服务器端后以1字节整数形式传递待运算数字个数。
[*]客户端向服务器端传递的每个整数型数据占用4字节。
[*]传递整数型数据后接着传递运算符。运算符信息占用1字节。
[*]选择字符 +、-、* 之一传递。
[*]服务器端以4字节整数型向客户端传回运算效果。
[*]客户端得到运算效果后终止与服务器端的连接。
这种水平的协议相称于实现了一半程序,这也说明应用层协议设计在网络编程中的重要性。只要设计好协议,实现程序就不会成为大问题。另外要记住的一点,调用close()函数将向通信对端传递 EOF,请各位记住这一点并加以运用。
op_client.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define OPSZ 4 // 操作数占用字节数
#define RLT_SIZE 4 // 运算结果数占用字节数
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char opmsg;
int result, opnd_cnt, i;
struct sockaddr_in serv_addr;
if (argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv);
serv_addr.sin_port = htons(atoi(argv));
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error!");
else
puts("Connected...........");
fputs("Operand count: ", stdout);
scanf("%d", &opnd_cnt); // 输入操作数个数
opmsg = (char)opnd_cnt; // 将操作符个数存入字符数组,占用1个字节
for (i = 0; i < opnd_cnt; i++)
{
printf("Operand %d: ", i + 1);
scanf("%d", (int *)&opmsg); // 将4字节整型数保存到字符数组中,需要将其转换成int指针类型
}
fgetc(stdin); // 标准输入一个字符
fputs("Operator: ", stdout); // 标准输出
scanf("%c", &opmsg); // 将操作符存入字符数组,占用1个字节
write(sock, opmsg, opnd_cnt * OPSZ + 2);// 发送数据给服务器端
read(sock, &result, RLT_SIZE); // 接收运算结果数据,存入result变量中
printf("Operation result: %ld\n", result);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
我们给出客户端向服务器端传送的数据的数据格式示例,如下图所示:
https://i-blog.csdnimg.cn/direct/eb53f43c072d4a0295cf135bfc085f62.png
可以看出,若想在同一数组中生存并传输多种数据结构,应把数组声明为char类型。而且需要额外做一些指针及数组运算。
op_server.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define OPSZ 4 // 操作数占用字节数
void error_handling(char *message);
int calculate(int opnum, int opnds[], char operator);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char opinfo = {0};
int result, opnd_cnt;
int recv_cnt, recv_len;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t clnt_addr_sz;
if (argc != 2)
{
printf("Usage: %s <port>\n", argv);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error!");
memset(serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv));
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error!");
if (listen(serv_sock, 5) == -1)
error_handling("listen() error!")
clnt_addr_sz = sizeof(clnt_addr);
for (i = 0; i < 5; i++)
{
opnd_cnt = 0;
clnt_sock = accept(serv_addr, (struct sockaddr *)&clnt_addr, &clnt_addr_sz);
read(clnt_sock, &opnd_cnt, 1); // 读取1字节操作数个数,存入opnd_cnt变量中
recv_len = 0;
while (opnd_cnt * OPSZ + 1 > recv_len) // 循环读取剩余的数据
{
recv_cnt = read(clnt_sock, &opinfo, BUF_SIZE);
recv_len += recv_cnt;
}
result = calculate(opnd_cnt, (int *)opinfo, opinfo);
write(clnt_sock, (char *)&result, sizeof(result)); // 向客户端传回运算结果消息
close(clnt_sock);
}
close(serv_sock);
return 0;
}
int calculate(int opnum, int opnds[], char op)
{
int result = opnds, i;
swith(op)
{
case '+':
for (i = 1; i < opnum; i++)
result += opnds;
break;
case '-':
for (i = 1; i < opnum; i++)
result -= opnds;
break;
case '*':
for (i = 1; i < opnum; i++)
result *= opnds;
break;
}
return result;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
TCP 原理
TCP套接字中的I/O缓冲
我们已经知道,TCP套接字的数据收发无边界。服务器端即使调用1次write函数传输40字节的数据,客户端也有大概通过调用4次read函数每次读取10字节。但此处也有一些疑问,服务器端一次性传输了40字节,而客户端居然可以缓慢地分批接收。客户端接收10字节后,剩下的30字节在何处期待呢?是不是像飞机为了等候着陆而在空中盘旋一样,剩下的30字节也在网络中徘徊并等等接收呢?
实际上,write函数调用后并非立刻传输数据,read函数调用后也并非立刻接收数据。更正确地说,如下图所示,write函数调用瞬间,数据被移至输出缓冲区(即发送缓冲区);read函数调用瞬间,从输入缓冲区(即接收缓冲区)读取数据。
https://i-blog.csdnimg.cn/direct/9adc874d9938454a8db05fdcab40080e.png
调用write函数时,数据被移至输出缓冲,在适当的时间(不管是分别发送还是一次性发送)传向对端的输入缓冲。这时对方将调用read函数从输入缓冲读取数据。这些I/O缓冲特性可整理如下:
[*]I/O缓冲在每个TCP套接字中单独存在。
[*]I/O缓冲在创建套接字时主动天生。
[*]即使关闭套接字也会继承传递输出缓冲中遗留的数据。
[*]关闭套接字将丢失输入缓冲(即接收缓冲)中的数据。
会不会有“客户端输入缓冲(即接收缓冲)为50字节,而服务器传输了100字节”的情况?
答:不会。TCP协议有流量控制机制,因此 “不会发生高出接收缓冲巨细的数据传输”。
所谓流量控制(flow control)就是让发送发的发送速率不要太快,要让接收方来得及接收。TCP协议利用滑动窗口(Sliding Window)机制来实现流量控制。
write函数并不是在向通信对端传输完全部数据时才返回,而是在数据被移到TCP套接字的发送缓冲时就返回了。但TCP会包管对发送缓冲数据的传输,以是说write函数在数据传输完成时返回,我们要正确明确这句话的真正内涵。
TCP内部工作原理1:与对方套接字的连接(三次握手)
TCP套接字从创建到消散所经历过程分为如下3步:
[*]与对方套接字建立连接。
[*]与对方套接字进行数据交换。
[*]断开与对方套接字的连接。
TCP在实际连接建立过程中会颠末3次对话过程。因此,该过程又称 “Three-way handshaking(三报文握手)”。接下来给出连接过程中实际交换的信息格式,如下图所示:
https://i-blog.csdnimg.cn/direct/2cd1d217b0034f119d08db356d87d33b.png
TCP套接字是以全双工(Full-duplex)方式工作的。也就是说,它可以双向传递数据,即可接收,也可发送。因此,正式收发数据前需要做一些准备工作。
[*]起首,哀求连接的主机A向主机B传递如下信息: SEQ: 1000, ACK: -
该消息中 SEQ为1000,ACK为空,而SEQ为1000的含义是:“现传递的数据报的初始序号为1000,假如接收无误,请通知我向您传递1001号数据包。”
这是首次哀求连接时使用的消息,又称SYN(Synchronization,同步),表现收发数据前传输的同步消息。
[*]接下来主机B向主机A传递如下消息: SEQ: 2000, ACK: 1001
此时SEQ为2000,ACK为1001,SEQ为2000的含义是:“现传递的数据包初始序号为2000,假如接收无误,请通知我向您传递2001号数据包。”而ACK: 1001 的含义是:“刚才传输的SEQ为1000的数据包接收无误,现在请传递SEQ为1001的数据包。”
对主机A首次传输的数据包的确认消息(ACK:1001)和为主机B传输数据做准备的同步消息(SEQ:2000)捆绑发送,因此,此种类型的消息又称为 SYN+ACK。
通信双方收发数据前向数据包分配初始序号,并向对方通知此序号,这都是为了防止数据丢失所做的准备。通过向数据包分配序号并确认,可以在数据丢失时立刻查看并重传丢失的数据包。因此,TCP可以包管可靠的数据传输。
[*]末了主机A向主机B传递如下消息: SEQ: 1001, ACK: 2001
由于主机A发送的 SYN 数据包需要斲丧一个序号,因此此刻主机A发送的第二个数据包的序号在之前的序号1000的基础上加1,也就是分配1001。此时该数据包传递的信息含义是:“已正确收到传输的SEQ为2000的数据包,现在可以传输SEQ为2001的数据包了。”
至此,主机A和主机B的TCP连接就建立成功了,接下来就可以进行数据传递操纵了。
TCP内部工作原理2:与对方主机的数据交换
通过第一步三报文握手过程成功建立起了TCP连接,完成了数据交换的准备工作,就下来就可以正式开始收发数据过程。
https://i-blog.csdnimg.cn/direct/5a4d83fb86ad469cb9440a094ac5f3bf.png
上图给出了主机A分2次(分2个TCP报文段)向主机B传递200字节数据的过程。起首,主机A通过第一个报文段发送100个字节的数据,报文段的SEQ为1200。主机B为了确认收到该报文段,向主机B发送 ACK 1301 确认。
此时的ACK号(确认号)为1301而非1201,原因在于ACK号的增量为传输的数据字节数。假设每次ACK号不加传输的字节数,这样虽然可以确认报文段的传输,但无法明确100字节数据是全部正确传递还是丢失了一部分。因此按如下公式传递ACK消息:
ACK号 = SEQ号 + 传递的数据字节数 + 1
与三报文握手过程雷同,末了加1是为了告知对方下次要传递的SEQ号。
传输数据过程中报文段丢失的情况,如下图所示:
https://i-blog.csdnimg.cn/direct/685f8f1c27a14782a4bbd8e3384e962e.png
上图表现通过SEQ 1301 报文段向主机B传递100字节的数据。但中间发生了错误,主机B并未收到。颠末一段时间后,主机A仍未收到对于 SEQ 1301 的ACK确认,因此主机A会重传该报文段。为了完成报文段的重传,TCP套接字会启动超时计时器以等候ACK应答。若超时计时器发生超时(Time-out)则重传。
TCP内部工作原理3:断开与套接字的连接(四次挥手)
先由套接字A向套接字B传递断开连接的消息,套接字B发出确认收到的消息,然后向套接字A传递可以断开连接的消息,套接字A同样发出确认消息,如下图所示:
https://i-blog.csdnimg.cn/direct/fda9c674fbc14bc4945f2ee101bf6e84.png
报文段内的 FIN 表现断开连接。也就是说,双方各发送1次 FIN 报文段后断开连接。SEQ 和 ACK 的含义与前面讲授的含义一样。在上图中,主机B向主机A传递了两次 ACK 5001,这是由于第二次FIN 报文段中的ACK 5001 只是由于接收ACK消息后未接收数据而重传给主机A的,以便其在要发出的第四个确认报文段中知晓本身的SEQ。
基于 Windows 的实现
op_client_win.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OP_SIZE 4
void ErrorHanding(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN serverAddr;
char op_msg;
int result, opndCnt;
if (argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHanding("WSAStartup() error!");
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHanding("hSocket() error!");
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(argv);
serverAddr.sin_port = htons(atoi(argv));
if (connect(hSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
ErrorHanding("connect() error!");
else
puts("Connected......");
fputs("Operand count: ", stdout);
scanf("%d", &opndCnt);
op_msg = (char)opndCnt;
for (int i = 0; i < opndCnt; i++)
{
printf("Operand %d: ", i + 1);
scanf("%d", (int *)&op_msg);
}
fgetc(stdin);
fputs("Operator: ", stdout);
scanf("%c", &op_msg);
send(hSocket, op_msg, opndCnt * OP_SIZE + 2, 0);
recv(hSocket, (char *)&result, RLT_SIZE, 0);
printf("Operation result: %d\n", result);
closesocket(hSocket);
WSACleanup();
return 0;
}
op_server_win.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
#define OP_SIZE 4
void ErrorHanding(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int calculate(int op_num, int op_info[], char op)
{
int result = op_info;
switch (op)
{
case '+':
for (int i = 1; i < op_num; i++)
result += op_info;
break;
case '-':
for (int i = 1; i < op_num; i++)
result -= op_info;
break;
case '*':
for (int i = 1; i < op_num; i++)
result *= op_info;
break;
}
return result;
}
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hServerSock, hClientSock;
SOCKADDR_IN serverAddr, clientAddr;
int clientAddrSize;
char op_info;
int recvCnt, recvLen;
int result, opndCnt;
if (argc != 2)
{
printf("Usage: %s <port>\n", argv);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHanding("WSAStartup() error!");
hServerSock = socket(PF_INET, SOCK_STREAM, 0);
if (hServerSock == INVALID_SOCKET)
ErrorHanding("socket() error!");
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(atoi(argv));
if (bind(hServerSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
ErrorHanding("bind() error!");
if (listen(hServerSock, 5) == SOCKET_ERROR)
ErrorHanding("listen() error!");
clientAddrSize = sizeof(clientAddr);
for (int i = 0; i < 5; i++)
{
opndCnt = 0;
hClientSock = accept(hServerSock, (SOCKADDR *)&clientAddr, &clientAddrSize);
if (hClientSock == INVALID_SOCKET)
ErrorHanding("accept() error!");
else
printf("Connect client %d\n", i + 1);
recv(hClientSock, (char *)&opndCnt, 1, 0);
recvLen = 0;
while (recvLen < (opndCnt * OP_SIZE + 1))
{
recvCnt = recv(hClientSock, &op_info, BUF_SIZE - 1, 0);
recvLen += recvCnt;
}
result = calculate(opndCnt, (int *)op_info, op_info);
send(hClientSock, (char *)&result, sizeof(result), 0);
closesocket(hClientSock);
}
closesocket(hServerSock);
WSACleanup();
return 0;
}
编译:
gcc op_server_win.c -lwsock32 -o opserv
gcc op_client_win.c -lwsock32 -o opclnt
运行效果:
// 服务器端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>opserv 9190
Connect client 1
Connect client 2
Connect client 3
// 客户端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>opclnt 127.0.0.1 9190
Connected......
Operand count: 2
Operand 1: 24
Operand 2: 12
Operator: -
Operation result: 12
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>opclnt 127.0.0.1 9190
Connected......
Operand count: 3
Operand 1: 12
Operand 2: 24
Operand 3: 36
Operator: +
Operation result: 72
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>opclnt 127.0.0.1 9190
Connected......
Operand count: 3
Operand 1: 2
Operand 2: 5
Operand 3: 10
Operator: *
Operation result: 100
习题
(1)请说明TCP套接字连接建立的三次握手过程。尤其是3次数据交换过程每次收发的数据内容。
https://i-blog.csdnimg.cn/direct/3620ec66991c4b1987be53a19bdd65f2.png
初始状态:客户端处于 Closed 的状态,服务端处于 Listen 状态,进行三次握手。
第一次握手:客户端给服务端发一个 SYN 报文段,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_SENT 状态。(在SYN报文段中同步位SYN=1,初始序号seq=x)SYN=1的报文段不能携带数据,但要斲丧掉一个序号。
第二次握手:服务器收到客户端的 SYN 报文段之后,会以本身的 SYN 报文段作为应答,并且也是指定了本身的初始化序列号 ISN(s)。同时会把客户端的 ISN(c) + 1 作为ACK 的值,表现本身已经收到了客户端的 SYN报文,此时服务器处于 SYN_RCVD 的状态。(在SYN ACK报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y)
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,固然,也是一样把服务器的 ISN(s) + 1 作为 ACK 的值,表现已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。(在ACK报文段中ACK=1,确认号ack=y+1,序号seq=x+1)
(2)TCP是可靠的数据传输协议,但在通过网络通信的过程大概丢失数据。请通过ACK和SEQ说明TCP通过何种机制包管丢失数据的可靠传输。
TCP通过在TCP报文段首部中设置SEQ(序号)和ACK(确认号)字段,就可以知道传输的数据是否正确地被通信对端接收。SEQ表现当前发送的TCP报文段的第一个数据字节的序号,ACK表现盼望收到对方下一个报文段的第一个数据字节的序号。当收到某个确认报文段时,若确认号ACK=N,则表明到序号 N-1 为止的全部数据对方都已正确收到。若等候确认报文段超时,则说明传输的数据大概丢失,需要重传。
(3)TCP 套接字中调用 write 和 read 函数时数据怎样移动?结合 I/O 缓冲进行说明。
一个TCP套接字是有独立地接收缓冲和发送缓存的,它们是操纵系统内核区分配的内存空间。当TCP套接字调用write函数时,就是将待发送数据移至TCP的发送缓冲区中,而调用read函数时,就是接收TCP的接收缓冲区中的数据。
(4)对方主机的输入缓冲剩余50字节空间时,若本方主机通过write函数哀求传输70字节,问TCP怎样处置惩罚这种情况?
通过TCP流量控制机制,对方主机会把输入缓冲巨细传送给本方主机。因此即使要求传送70字节的数据,本方主机也不会传输高出50字节数据,剩余的部分生存在传输方的输出缓冲中,等候对方主机的输入缓冲有空余空间时再传输剩余数据。
这种交换缓冲区多余空间信息的协议被称为滑动窗口协议。
(5)第2章示例tcp_server.c(第一章的hello_server.c)和tcp_client.c中,客户端接收服务器端传输的字符串后便退出。现更改程序,使服务器端和客户端各传送1次字符串。考虑到使用TCP协议,以是传输字符串前先以4字节整数型方式传递字符串长度。连接时服务器端和客户端数据传输格式如下。
https://i-blog.csdnimg.cn/direct/a65212727a034eed8a2c16ecb2604720.png
另外,不限定字符串传输顺序及种类,但必须进行3次数据交换。
(6)创建收发文件的服务器/客户端程序,实现顺序如下。
[*]客户端接收用户输入的传输文件名。
[*]客户端哀求服务器传输该文件名所指的文件。
[*]假如该文件存在,服务器端就将其发送给客户端;反之,则断开连接(复兴文件不存在的提示信息)。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]