《TCP/IP网络编程》学习笔记 | Chapter 5:基于TCP的服务器端/客户端(2)
- 《TCP/IP网络编程》学习笔记 | Chapter 5:基于TCP的服务器端/客户端(2)
- 回声客户端的完美实现
- TCP 原理
- 基于 Windows 的实现
- 习题
- (1)请说明TCP套接字连接建立的三次握手过程。尤其是3次数据交换过程每次收发的数据内容。
- (2)TCP是可靠的数据传输协议,但在通过网络通信的过程可能丢失数据。请通过ACK和SEQ说明TCP通过何种机制保证丢失数据的可靠传输。
- (3)TCP 套接字中调用 write 和 read 函数时数据如何移动?结合 I/O 缓冲进行说明。
- (4)对方主机的输入缓冲剩余50字节空间时,若本方主机通过write函数请求传输70字节,问TCP如何处理这种情况?
- (5)第2章示例tcp_server.c(第一章的hello_server.c)和tcp_client.c中,客户端接收服务器端传输的字符串后便退出。现更改程序,使服务器端和客户端各传送1次字符串。考虑到使用TCP协议,所以传输字符串前先以4字节整数型方式传递字符串长度。连接时服务器端和客户端数据传输格式如下。
- (6)创建收发文件的服务器/客户端程序,实现顺序如下。
《TCP/IP网络编程》学习笔记 | Chapter 5:基于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[str_len] = 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[BUF_SIZE];
int str_len, recv_len, recv_cnt;
struct sockaddr_in serv_adr;
if (argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
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[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
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[recv_len], BUF_SIZE - 1);
if (recv_cnt == -1)
error_handling("read() error");
recv_len += recv_cnt;
}
message[str_len] = 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[BUF_SIZE];
int result, opnd_cnt, i;
struct sockaddr_in serv_addr;
if (argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
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[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
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[0] = (char)opnd_cnt; // 将操作符个数存入字符数组,占用1个字节
for (i = 0; i < opnd_cnt; i++)
{
printf("Operand %d: ", i + 1);
scanf("%d", (int *)&opmsg[i * OPSZ + 1]); // 将4字节整型数保存到字符数组中,需要将其转换成int指针类型
}
fgetc(stdin); // 标准输入一个字符
fputs("Operator: ", stdout); // 标准输出
scanf("%c", &opmsg[opnd_cnt * OPSZ + 1]); // 将操作符存入字符数组,占用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);
}
我们给出客户端向服务器端传送的数据的数据格式示例,如下图所示:
可以看出,若想在同一数组中保存并传输多种数据结构,应把数组声明为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[BUF_SIZE] = {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[0]);
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[1]));
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[recv_len], BUF_SIZE);
recv_len += recv_cnt;
}
result = calculate(opnd_cnt, (int *)opinfo, opinfo[recv_len - 1]);
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[0], i;
swith(op)
{
case '+':
for (i = 1; i < opnum; i++)
result += opnds[i];
break;
case '-':
for (i = 1; i < opnum; i++)
result -= opnds[i];
break;
case '*':
for (i = 1; i < opnum; i++)
result *= opnds[i];
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函数调用瞬间,从输入缓冲区(即接收缓冲区)读取数据。
调用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(三报文握手)”。接下来给出连接过程中实际交换的信息格式,如下图所示:
TCP套接字是以全双工(Full-duplex)方式工作的。也就是说,它可以双向传递数据,即可接收,也可发送。因此,正式收发数据前需要做一些准备工作。
- 首先,请求连接的主机A向主机B传递如下信息:
[SYN] SEQ: 1000, ACK: -
该消息中 SEQ为1000,ACK为空,而SEQ为1000的含义是:“现传递的数据报的初始序号为1000,如果接收无误,请通知我向您传递1001号数据包。”
这是首次请求连接时使用的消息,又称SYN(Synchronization,同步),表示收发数据前传输的同步消息。
- 接下来主机B向主机A传递如下消息:
[SYN+ACK] 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传递如下消息:
[ACK] SEQ: 1001, ACK: 2001
因为主机A发送的 SYN 数据包需要消耗一个序号,因此此刻主机A发送的第二个数据包的序号在之前的序号1000的基础上加1,也就是分配1001。此时该数据包传递的信息含义是:“已正确收到传输的SEQ为2000的数据包,现在可以传输SEQ为2001的数据包了。”
至此,主机A和主机B的TCP连接就建立成功了,接下来就可以进行数据传递操作了。
TCP内部工作原理2:与对方主机的数据交换
通过第一步三报文握手过程成功建立起了TCP连接,完成了数据交换的准备工作,就下来就可以正式开始收发数据过程。
上图给出了主机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号。
传输数据过程中报文段丢失的情况,如下图所示:
上图表示通过SEQ 1301 报文段向主机B传递100字节的数据。但中间发生了错误,主机B并未收到。经过一段时间后,主机A仍未收到对于 SEQ 1301 的ACK确认,因此主机A会重传该报文段。为了完成报文段的重传,TCP套接字会启动超时计时器以等待ACK应答。若超时计时器发生超时(Time-out)则重传。
TCP内部工作原理3:断开与套接字的连接(四次挥手)
先由套接字A向套接字B传递断开连接的消息,套接字B发出确认收到的消息,然后向套接字A传递可以断开连接的消息,套接字A同样发出确认消息,如下图所示:
报文段内的 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[BUF_SIZE];
int result, opndCnt;
if (argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
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[1]);
serverAddr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
ErrorHanding("connect() error!");
else
puts("Connected......");
fputs("Operand count: ", stdout);
scanf("%d", &opndCnt);
op_msg[0] = (char)opndCnt;
for (int i = 0; i < opndCnt; i++)
{
printf("Operand %d: ", i + 1);
scanf("%d", (int *)&op_msg[i * OP_SIZE + 1]);
}
fgetc(stdin);
fputs("Operator: ", stdout);
scanf("%c", &op_msg[opndCnt * OP_SIZE + 1]);
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[0];
switch (op)
{
case '+':
for (int i = 1; i < op_num; i++)
result += op_info[i];
break;
case '-':
for (int i = 1; i < op_num; i++)
result -= op_info[i];
break;
case '*':
for (int i = 1; i < op_num; i++)
result *= op_info[i];
break;
}
return result;
}
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hServerSock, hClientSock;
SOCKADDR_IN serverAddr, clientAddr;
int clientAddrSize;
char op_info[BUF_SIZE];
int recvCnt, recvLen;
int result, opndCnt;
if (argc != 2)
{
printf("Usage: %s <port>\n", argv[0]);
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[1]));
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[recvLen], BUF_SIZE - 1, 0);
recvLen += recvCnt;
}
result = calculate(opndCnt, (int *)op_info, op_info[recvLen - 1]);
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次数据交换过程每次收发的数据内容。
初始状态:客户端处于 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字节整数型方式传递字符串长度。连接时服务器端和客户端数据传输格式如下。
另外,不限制字符串传输顺序及种类,但必须进行3次数据交换。
答:
先制定通信双方的协议:
- 服务器端和客户端分别向对方发送3个字符串。
- 传输字符串前先以4字节整数型方式传递字符串长度。
- 服务器端先发。
5_5_server.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void ErrorHanding(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hServerSock, hClientSock;
SOCKADDR_IN serverAddr, clientAddr;
int clientAddrSize;
char msg1[] = "Hello client!";
char msg2[] = "I'm server.";
char msg3[] = "Nice to meet you.";
char *str_arr[] = {msg1, msg2, msg3}; // 指针数组,保存三个字符串指针
int strLen;
char read_buf[BUF_SIZE];
if (argc != 2)
{
printf("Usage: %s <port>\n", argv[0]);
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[1]));
if (bind(hServerSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
ErrorHanding("bind() error!");
if (listen(hServerSock, 5) == SOCKET_ERROR)
ErrorHanding("listen() error!");
clientAddrSize = sizeof(clientAddr);
hClientSock = accept(hServerSock, (SOCKADDR *)&clientAddr, &clientAddrSize);
if (hClientSock == INVALID_SOCKET)
ErrorHanding("accept() error!");
for (int i = 0; i < 3; i++)
{
strLen = strlen(str_arr[i]) + 1;
send(hClientSock, (char *)&strLen, 4, 0);
send(hClientSock, str_arr[i], strLen, 0);
recv(hClientSock, (char *)&strLen, 4, 0);
recv(hClientSock, read_buf, strLen, 0);
printf("%s\n", read_buf);
}
closesocket(hClientSock);
closesocket(hServerSock);
WSACleanup();
return 0;
}
5_5_client.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
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 msg1[] = "Hello server!";
char msg2[] = "I'm client.";
char msg3[] = "Nice to meet you too!";
char *str_arr[] = {msg1, msg2, msg3}; // 指针数组,保存三个字符串指针
int strLen;
char read_buf[BUF_SIZE];
if (argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
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[1]);
serverAddr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
ErrorHanding("connect() error!");
else
puts("Connected......");
for (int i = 0; i < 3; i++)
{
recv(hSocket, (char *)&strLen, 4, 0);
recv(hSocket, read_buf, strLen, 0);
printf("%s\n", read_buf);
strLen = strlen(str_arr[i]) + 1;
send(hSocket, (char *)&strLen, 4, 0);
send(hSocket, str_arr[i], strLen, 0);
}
closesocket(hSocket);
WSACleanup();
return 0;
}
编译:
gcc 5_5_server.c -lwsock32 -o 5_5_server
gcc 5_5_client.c -lwsock32 -o 5_5_client
运行结果:
// 服务器端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>5_5_server 9190
Hello server!
I'm client.
Nice to meet you too!
// 客户端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>5_5_client 127.0.0.1 9190
Connected......
Hello client!
I'm server.
Nice to meet you.
(6)创建收发文件的服务器/客户端程序,实现顺序如下。
- 客户端接收用户输入的传输文件名。
- 客户端请求服务器传输该文件名所指的文件。
- 如果该文件存在,服务器端就将其发送给客户端;反之,则断开连接(回复文件不存在的提示信息)。
答:
5_6_server.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
#define FILE_NAME_SIZE 30
void ErrorHanding(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET serverSock, clientSock;
SOCKADDR_IN serverAddr, clientAddr;
int clientAddrSize;
int read_cnt;
char file_name[FILE_NAME_SIZE];
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHanding("WSAStartup() error!");
serverSock = socket(PF_INET, SOCK_STREAM, 0);
if (serverSock == 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[1]));
if (bind(serverSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
ErrorHanding("bind() error!");
if (listen(serverSock, 5) == SOCKET_ERROR)
ErrorHanding("listen() error!");
clientAddrSize = sizeof(clientAddr);
clientSock = accept(serverSock, (SOCKADDR *)&clientAddr, &clientAddrSize);
if (clientSock == INVALID_SOCKET)
ErrorHanding("accept() error!");
recv(clientSock, file_name, FILE_NAME_SIZE, 0);
FILE *fp = fopen(file_name, "rb");
if (fp != NULL)
{
while (1)
{
read_cnt = fread((void *)buf, 1, BUF_SIZE, fp);
if (read_cnt < BUF_SIZE)
{
send(clientSock, buf, read_cnt, 0);
break;
}
else
send(clientSock, buf, BUF_SIZE, 0);
}
}
fclose(fp);
closesocket(clientSock);
closesocket(serverSock);
WSACleanup();
return 0;
}
5_6_client.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
#define FILE_NAME_SIZE 30
void ErrorHanding(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET sock;
SOCKADDR_IN serverAddr;
int str_len;
char file_name[FILE_NAME_SIZE];
char read_buf[BUF_SIZE];
if (argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHanding("WSAStartup() error!");
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET)
ErrorHanding("sock() error!");
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
serverAddr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
ErrorHanding("connect() error!");
printf("Input file name: ");
scanf("%s", file_name);
send(sock, file_name, strlen(file_name) + 1, 0);
while ((str_len = recv(sock, read_buf, BUF_SIZE, 0)) != 0)
printf("%s", read_buf);
closesocket(sock);
WSACleanup();
return 0;
}
gcc 5_6_server.c -lwsock32 -o 5_6_server
gcc 5_6_client.c -lwsock32 -o 5_6_client
运行结果:
// 服务器端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>5_6_server 9190
// 客户端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>5_6_client 127.0.0.1 9190
Input file name: data.txt
We're no strangers to love
You know the rules and so do I
A full commitment's what I'm thinking of
You wouldn't get this from any other guy
I just wanna tell you how I'm feeling
Gotta make you understand
Never gonna give you up
Never gonna let you down
Never gonna run around and desert you
Never gonna make you cry
Never gonna say goodbye
Never gonna tell a lie and hurt you