TCP/IP网络编程之基于TCP的服务端/客户端(二)

回声客户端问题

上一章TCP/IP网络编程之基于TCP的服务端/客户端(一)中,我们解释了回声客户端所存在的问题,那么单单是客户端的问题,服务端没有任何问题?是的,服务端没有问题,现在先让我们回顾下服务端的I/O代码

echo_server.c

……
while ((str_len = read(clnt_sock, messag, 1024)) != 0)write(clnt_sock, messag, str_len);
……

    

接着,我们回顾客户端的代码

echo_client.c

……
write(sock, message, strlen(message));
str_len = read(sock, message, 1024 - 1);
……

  

二者都在循环调用read或write函数,实际上之前的回声客户端将100%接收自己传输的数据,只不过接收数据时的单位有些问题

观察下面的代码,我们可以知道回声客户端传输的是字符串,而且是通过调用write函数一次性发送,之后还调用了一次read函数,期待着接收自己传输的字符串,这就是问题所在

echo_client.c

……
while (1)
{fputs("Input message(Q to quit):", stdout);fgets(message, 1024, stdin);if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))break;write(sock, message, strlen(message));str_len = read(sock, message, 1024 - 1);message[str_len] = 0;printf("Message from server:%s", message);
}
……

  

既然回声客户端会收到所有字符串数据,是否只需多等一会?过一段时间再调用read函数是否可以一次性读取所有的字符串内容?的确,过一段时间后即可接收,但需要多久?1秒还是1分钟?没人知道。而且这也不符合常理,正常应该客户端在收到字符串数据时立即读取并输出

其实问题很容易解决,因为可以提前确定接收数据的长度,若之前传输了20个字节长的字符串,则在接收时循环调用read函数读取20个字节即可,下面,我们解决方案的代码

echo_client2.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
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");elseputs("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[recv_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);
}

  

在45~56行是变更及添加部分,echo_client.c仅调用了一次read函数,上述示例为了接收所有传输数据而循环调用read函数。另外代码48行还可以写成如下形式:

while (recv_len != str_len)
{……
}

  

接收的数据大小应和传输的相同,因此recv_len中保存的值等于str_len保存的值时,即可跳出while循环

回声客户端可以提前知道接收数据的长度,但这只是特例,多数情况下,我们是不知道接收数据的长度,那么我们应该如何收发数据?此时需要的就是应用层协议的定义,之前回声服务端/客户端中曾经定义:收到Q就立即终止连接

同样,收发数据过程中也需要定好规则(协议)以表示数据的边界,或提前告知收发数据的大小。服务端/客户端实现过程中逐步定义的这些规则集合就是应用层协议,可以看出应用层协议并不是什么高深的技术,仅仅是为了特定程序而制定的规则

下面我们就来编写一个应用层协议的程序,该程序中服务端从客户端获得多个数字和运算符信息。服务器端收到这些信息后根据运算符对数字做处理再返回给客户端,例如:客户端向服务端传递3、5、9的同时请求加法运算,那么服务端做完3+5+9=17的结果后会返回给客户端

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 RLT_SIZE 4
#define OPSZ 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_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!");elseputs("Connected......");fputs("Operand count:", stdout);scanf("%d", &opnd_cnt);opmsg[0] = (char)opnd_cnt;for (i = 0; i < opnd_cnt; i++){printf("Operand %d:", i + 1);scanf("%d", (int *)&opmsg[i * OPSZ + 1]);}fgetc(stdin);fputs("Operator:", stdout);scanf("%c", &opmsg[opnd_cnt * OPSZ + 1]);write(sock, opmsg, opnd_cnt * OPSZ + 2);read(sock, &result, RLT_SIZE);printf("Operation result:%d\n", result);close(sock);return 0;
}
void error_handling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

  

  • 第8、9行:将待计算的数字的字节数和运算结果的字节数设为常数
  • 第14行:为收发数据准备的内存空间,需要数据积累到一定程度后再收发,因此通过数组创建
  • 第37、38行:从用户的输入中得到待算数个数后,保存至数组opmsg。强制转换成char类型,因为协议规定待算数个数应通过一个字节整数型传递,因此不能超过一个字节整数型能够表示的范围。示例中用的是有符号整数型,但待算数个数不能是负数,因此使用无符号整数型更合理
  • 第40~44行:从用户的输入中得到待算整数,保存到数组opmsg。4字节int型数据要保存到保存到char数组,因而在转换成int指针类型
  • 第45行:第47行中输入字符,在此之前调用fgetc函数删掉缓冲中的字符'\n'
  • 第47行:最后输入运算符信息,保存到opmsg数组
  • 第48行:调用write函数一次性传输opmsg数组中的运算相关信息,可以调用一次write函数进行传输,也可以分多次调用
  • 第49行:保存服务端传输的运算结果,待接收的数据长度为4字节,因此调用一次read函数即可接收

 

 

图1-1   客户端op_client.c的数据传送格式

从图1-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];int result, opnd_cnt, i;int recv_cnt, recv_len;struct sockaddr_in serv_adr, clnt_adr;socklen_t clnt_adr_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_adr, 0, sizeof(serv_adr));serv_adr.sin_family = AF_INET;serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);serv_adr.sin_port = htons(atoi(argv[1]));if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)error_handling("bind() error");if (listen(serv_sock, 5) == -1)error_handling("listen() error");clnt_adr_sz = sizeof(clnt_adr);for (i = 0; i < 5; i++){opnd_cnt = 0;clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);read(clnt_sock, &opnd_cnt, 1);recv_len = 0;while ((opnd_cnt * OPSZ + 1) > recv_len){recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE - 1);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;switch (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);
}

  

  • 第38行:为了接收5个客户端的连接请求而编写的for语句
  • 第42行:首先接收待算数个数
  • 第45~49行:根据第42行中的待算数个数接收待算数
  • 第50行:调用calculate函数的同时传递待算数和运算符信息参数
  • 第51行:向客户端传输calculate函数返回的运算结果

编译op_server.c并运行

# gcc op_server.c -o op_server
# ./op_server 8500

  

编译op_client.c并运行

# gcc op_client.c -o op_client
# ./op_client 127.0.0.1 8500
Connected......
Operand count:3
Operand 1:1
Operand 2:2
Operand 3:3
Operator:+
Operation result:6
# ./op_client 127.0.0.1 8500
Connected......
Operand count:2
Operand 1:2
Operand 2:15
Operator:*
Operation result:30

  

TCP原理

之前我们说过,TCP套接字的数据收发无边界,服务端即使调用一次write函数传输60个字节的数据,客户端也有可能分3次调用read函数,每次读取20个字节的数据。但此处也有些疑问,服务端一次性传输60个字节的数据,而客户端分批读取。客户端在读取20个字节后,剩下的40个字节在何处等候呢?实际上,write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据。更准确地说,图1-2所示,write函数调用瞬间,数据将移至输出缓冲;read函数调用瞬间,从输入缓冲读取数据

图1-2   TCP套接字的I/O缓冲

如图1-2所示,调用write函数时,数据将移到输出缓冲,在适当的时候(不管是分别传送还是一次性传送)传向对方的输入缓冲。这时对方将调用read函数从输入缓冲读取数据。这些I/O缓冲特性如下:

  • I/O缓冲在每个TCP套接字中单独存在
  • I/O缓冲在创建套接字时自动生成
  • 即使关闭套接字也会继续传递输出缓冲中遗留的数据
  • 关闭套接字将丢失输入缓冲中的数据

如果客户端输入缓冲有50个字节,但服务端却有100个字节需要传输,是否会造成数据丢失?之前说过TCP是可靠的,不会发生超过输入缓冲大小的数据传输,因为TCP会控制数据流,其中有滑动窗口协议,接收数据的套接字每次会告诉发送数据的套接字可传递的最大字节数,于是发送数据的套接字收到这个数字后传递等长度大小的数据,待接收数据的套接字发现缓冲中腾出更多位置,会告诉发送数据的套接字,接收更多的数据。因此,TCP不会因为缓冲满了而丢失数据

TCP内部工作原理1:与对方套接字的连接

TCP套接字从创建到消失所经过程分为如下三步:

  1. 与对方套接字建立连接
  2. 与对方套接字进行数据交换
  3. 断开与对方套接字的连接

TCP在实际通信过程中也会经历三次对话过程,因此,该过程又称为Three-way handshake(三次握手),如图1-3所示:

图1-3   TCP套接字的连接设置过程

套接字是以双全工方式工作的,也就是说它可以双向传递数据。因此,收发数据前需要做一些准备,首先,请求连接的主机A向主机B传递如下消息:

[SYN] SEQ:1000, ACK:-

该消息中SEQ为1000,ACK为空,而SEQ为1000的含义是:主机A现传递的数据包序号为1000给主机B,如果接收无误,请通知主机A向主机B传递1001号数据包。这是首次请求连接时使用的消息,又称SYN。SYN是Synchronize的简写,表示收发数据前传输的同步消息。接下来,主机B向主机A传递消息:[SYN+ACK] SEQ:2000, ACK:1001。此时SEQ为2000,ACK为1001,而SEQ具体含义和之前一样:主机B现传递的数据包序号为2000给主机A,如果接收无误,请通知主机B向主机A传递2001号数据包

而ACK 1001的含义为:刚才传输的SEQ为1000的数据包接收无误,请传递SEQ为1001的数据包。对于主机A首次传输的数据包的确认消息(ACK 1001)和主机B传输数据做准备的同步消息(SEQ 2000)捆绑发送,因此,此种类型的消息又称SYN+ACK

收发数据前向数据包分配序号,并向对方通报此序号,这都是为防止数据丢失所做的准备。通过向数据包分配序号并确认,可以在数据丢失时马上查看并重传丢失的数据包。因此,TCP可以保证可靠的数据传输。最后观察主机A向主机B传输消息:[ACK] SEQ: 1001, ACK: 2001。我们都明白SEQ和ACK的含义了,主机A现在向主机B传递1001号数据包,并表示2001号数据包接收无误。至此,主机A和主机B确认了彼此均就绪

TCP内部工作原理2:与对方主机的数据交换

通过第一步三次握手过程完成了数据交换准备,下面就开始正式收发收据。其默认方式如图1-4所示:

图1-4   TCP套接字的数据交换过程

图1-4给出了主机A分两次向主机B传输各100字节的过程。首先主机A通过一个数据包发送100个字节的数据,数据包的SEQ为1200。主机B为了确认这一点,向主机A发送ACK1301消息。此时的ACK号为1301而非1201,原因在于ACK号的增量为传输的数据字节数。假设每次ACK号不加传输的字节数,这样虽然可以确认数据包到达目标主机,但无法明确100字节全部正确到达还是丢失了一部分。因此ACK消息公式为:ACK号 = SEQ号 + 传递字节数 + 1

与三次握手协议相同,最后加一是为了告知对方下次要传递的SEQ号。下面分析传输过程中数据包消失的情况,如图1-5:

如图1-5   TCP套接字数据传输过程中发生错误

图1-5表示通过SEQ 1301数据包向主机B传递100字节数据。但中间发生了错误,主机B未收到,经过一段时间,主机A仍为收到对于SEQ 1301的ACK确认,因此试着重传该数据包。为了完成数据包的重传,TCP套接字启动计时器以等待ACK应答。若计时器发生超时则重传

TCP的内部工作原理3:断开与套接字的连接

TCP套接字的结束过程也与之前相似,如果对方还有数据需要传输时直接断掉连接会出现问题,所以断开连接时需要双方协商,先由套接字A向套接字B传递断开连接的消息,套接字B发出确认收到的消息,然后向套接字A传递可以断开连接的消息,套接字A同样发出确认的消息,如图1-6所示:

图1-6   TCP套接字断开连接过程

图1-6数据包内的FIN表示断开连接。也就是说,双方各发送一次FIN消息后断开连接,此过程经历四个阶段,因此称为四次握手(Four-way handshaking)。图1-6向主机A传递两次ACK 5001,也许这会让大家感到困惑,这里做一下解答:主机A向主机B发送FIN消息,告诉主机B自己没有数据可以发送,但如果主机B还有数据没发送完,可以不必急着关闭套接字,可以继续发送数据。于是,主机B发送ACK。当主机B确认数据发送完毕,则向主机A发送FIN消息,告诉主机A数据全部发送关闭,准备关闭连接。主机A收到FIN消息,还是不相信网络,怕主机B不知道要关闭,所以发送ACK,如果主机B没有收到可以重传。主机B收到ACK后,就知道可以断开连接了,于是主机A等待在超时时间内没有收到回复, 则证明主机B已关闭连接,于是主机A也跟着关闭连接

转载于:https://www.cnblogs.com/beiluowuzheng/p/9656284.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/388498.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

谈谈iOS获取调用链

本文由云社区发表iOS开发过程中难免会遇到卡顿等性能问题或者死锁之类的问题&#xff0c;此时如果有调用堆栈将对解决问题很有帮助。那么在应用中如何来实时获取函数的调用堆栈呢&#xff1f;本文参考了网上的一些博文&#xff0c;讲述了使用mach thread的方式来获取调用栈的步…

python 移动平均线_Python中的移动平均线

python 移动平均线There are situations, particularly when dealing with real-time data, when a conventional average is of little use because it includes old values which are no longer relevant and merely give a misleading impression of the current situation.…

html5字体的格式转换,font字体

路由器之家网今天精心准备的是《font字体》&#xff0c;下面是详解&#xff01;html中的标签是什么意思HTML提供了文本样式标记&#xff0c;用来控制网页中文本的字体、字号和颜色&#xff0c;多种多样的文字效果可以使网页变得更加绚丽。其基本语法格式&#xff1a;文本内容fa…

红星美凯龙牵手新潮传媒抢夺社区消费市场

瞄准线下流量红利&#xff0c;红星美凯龙牵手新潮传媒抢夺社区消费市场 中新网1月14日电 2019年1月13日&#xff0c;红星美凯龙和新潮传媒战略合作发布会在北京召开&#xff0c;双方宣布建立全面的战略合作伙伴关系。未来&#xff0c;新潮传媒的梯媒产品将入驻红星美凯龙的全国…

机器学习 啤酒数据集_啤酒数据集上的神经网络

机器学习 啤酒数据集Artificial neural networks (ANNs), usually simply called neural networks (NNs), are computing systems vaguely inspired by the biological neural networks that constitute animal brains.人工神经网络(ANN)通常简称为神经网络(NNs)&#xff0c;是…

ER TO SQL语句

ER TO SQL语句的转换&#xff0c;在数据库设计生命周期的位置如下所示。 一、转换的类别 从ER图转化得到关系数据库中的SQL表&#xff0c;一般可分为3类&#xff1a; 1&#xff09;转化得到的SQL表与原始实体包含相同信息内容。该类转化一般适用于&#xff1a; 二元“多对多”关…

dede 5.7 任意用户重置密码前台

返回了重置的链接&#xff0c;还要把&amp删除了&#xff0c;就可以重置密码了 结果只能改test的密码&#xff0c;进去过后&#xff0c;这个居然是admin的密码&#xff0c;有点头大&#xff0c;感觉这样就没有意思了 我是直接上传的一句话&#xff0c;用菜刀连才有乐趣 OK了…

nasa数据库cm1数据集_获取下一个地理项目的NASA数据

nasa数据库cm1数据集NASA provides an extensive library of data points that they’ve captured over the years from their satellites. These datasets include temperature, precipitation and more. NASA hosts this data on a website where you can search and grab in…

r语言处理数据集编码_在强调编码语言或工具之前,请学习这3个基本数据概念

r语言处理数据集编码重点 (Top highlight)I got an Instagram DM the other day that really got me thinking. This person explained that they were a data analyst by trade, and had years of experience. But, they also said that they felt that their technical skill…

HTML和CSS面试问题总结,html和css面试总结

html和cssw3c 规范结构化标准语言样式标准语言行为标准语言1) 盒模型常见的盒模型有w3c盒模型(又名标准盒模型)box-sizing:content-box和IE盒模型(又名怪异盒模型)box-sizing:border-box。标准盒子模型&#xff1a;宽度内容的宽度(content) border padding margin低版本IE盒子…

山师计算机专业研究生怎么样,山东师范大学有计算机专业硕士吗?

山东师范大学位于山东省济南市&#xff0c;学校是一所综合性高等师范院校。该院校深受广大报考专业硕士学员的欢迎&#xff0c;因此很多学员想要知道山东师范大学有没有计算机专业硕士&#xff1f;山东师范大学是有计算机专业硕士的。下面就和大家介绍一下培养目标有哪些&#…

使用TensorFlow概率预测航空乘客人数

TensorFlow Probability uses structural time series models to conduct time series forecasting. In particular, this library allows for a “scenario analysis” form of modelling — whereby various forecasts regarding the future are made.TensorFlow概率使用结构…

python画激活函数图像

导入必要的库 import math import matplotlib.pyplot as plt import numpy as np import matplotlib as mpl mpl.rcParams[axes.unicode_minus] False 绘制softmax函数图像 fig plt.figure(figsize(6,4)) ax fig.add_subplot(111) x np.linspace(-10,10) y sigmoid(x)ax.s…

pdf.js插件使用记录,在线打开pdf

pdf.js插件使用记录&#xff0c;在线打开pdf 原文:pdf.js插件使用记录&#xff0c;在线打开pdf天记录一个js库&#xff1a;pdf.js。主要是实现在线打开pdf功能。因为项目需求需要能在线查看pdf文档&#xff0c;所以就研究了一下这个控件。 有些人很好奇&#xff0c;在线打开pdf…

程序员 sql面试_非程序员SQL使用指南

程序员 sql面试Today, the word of the moment is DATA, this little combination of 4 letters is transforming how all companies and their employees work, but most people don’t really know how data behaves or how to access it and they also think that this is j…

r a/b 测试_R中的A / B测试

r a/b 测试什么是A / B测试&#xff1f; (What is A/B Testing?) A/B testing is a method used to test whether the response rate is different for two variants of the same feature. For instance, you may want to test whether a specific change to your website lik…

Java基础回顾

内容&#xff1a; 1、Java中的数据类型 2、引用类型的使用 3、IO流及读写文件 4、对象的内存图 5、this的作用及本质 6、匿名对象 1、Java中的数据类型 Java中的数据类型有如下两种&#xff1a; 基本数据类型: 4类8种 byte(1) boolean(1) short(2) char(2) int(4) float(4) l…

计算机部分应用显示模糊,win10系统打开部分软件字体总显示模糊的解决方法-电脑自学网...

win10系统打开部分软件字体总显示模糊的解决方法。方法一&#xff1a;win10软件字体模糊1、首先&#xff0c;在Win10的桌面点击鼠标右键&#xff0c;选择“显示设置”。2、在“显示设置”的界面下方&#xff0c;点击“高级显示设置”。3、在“高级显示设置”的界面中&#xff0…

Tomcat调节

Tomcat默认可以使用的内存为128MB&#xff0c;在较大型的应用项目中&#xff0c;这点内存是不够的&#xff0c;需要调大,并且Tomcat本身不能直接在计算机上运行&#xff0c;需要依赖于硬件基础之上的操作系统和一个java虚拟机。 AD&#xff1a; 这里向大家描述一下如何使用Tom…

turtle 20秒画完小猪佩奇“社会人”

转载&#xff1a;https://blog.csdn.net/csdnsevenn/article/details/80650456 图片源自网络 作者 丁彦军 如需转载&#xff0c;请联系原作者授权。 今年社交平台上最火的带货女王是谁&#xff1f;范冰冰&#xff1f;杨幂&#xff1f;Angelababy&#xff1f;不&#xff0c;是猪…