首先要看懂TCP的传输结构,至于那理想七层模型与实际的四层模型就不先说了,后面再补上。
我是提倡先会用,在会用的基础上去理解。
可以理解为TCP之间的数据传输都是依赖各自的socket,socket就充当传输的中介吧。
而每个socket都对应两个缓冲区,一个输入缓冲区,一个输出缓冲区 。
怎么理解呢,且看下面的代码例子。
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //获取一个socket AF_INET表示IPV4 SOCK_STREAM表示基于TCP的数据传输
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666); //服务器端口
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //服务器ip, inet_addr用于IPv4的IP转换(十进制转换为二进制)
//连接服务器, 成功返回0, 错误返回 -1
if(connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
perror("connect");
exit(1);
}
char buffer[1024]; //定义一个buf
memset(buffer,'C',9); //填充9个'C'
memset(&buffer[9],'\0',1); //再在后面填充一个'\0' 字符串结束位
std::cout << buffer << std::endl; //打印一下验证
send(sockfd, buffer, 9, 0); //发送9个字节,这里没有包括'\0'
std::cout << "send over!" << std::endl;
close(sockfd);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#define QUEUE 20 //连接请求队列
int fd;
int main()
{
int socket_ = socket(AF_INET, SOCK_STREAM, 0); //若成功则返回一个sockfd (套接字描述符)
struct sockaddr_in server_sockaddr; //一般是储存地址和端口,用于信息的显示及存储作用
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(6666); //将一个无符号短整型数值转换为网络字节序,即大端模式
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示本地任意IP地址
if(bind(socket_, (struct sockaddr*)&server_sockaddr, sizeof(server_sockaddr)) == -1)
{
perror("bind");
exit(1);
}
if(listen(socket_, QUEUE) == -1)
{
perror("listen");
exit(1);
}
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
fd = accept(socket_, (struct sockaddr*)&client_addr, &length);
if( fd < 0 )
{
perror("connect");
exit(1);
}
char buffer[1024];
char buffer2[1024];
memset(buffer, 0, sizeof(buffer));
memset(buffer, 0, sizeof(buffer));
int len = read(fd, buffer, 5); //从读取缓冲区里读取5个字节
std::cout <<"len:" << len << " " << buffer << std::endl;
len = recv(fd, buffer2, 1024, 0); //从读取缓冲区里读取1024个字节(如果缓冲区里有1024数据的话)
std::cout <<"len:" << len << " " << buffer2 << std::endl;
close(fd); //关闭套接字
return 0;
}
关于创建socket和connect等等函数就不先多说了,外面资料一大堆。
下面就来根据缓冲区来说说,send()/write() 与 recv()/read() 两个函数吧。
send(sockfd, buffer, 9, 0); //发送9个字节,这里没有包括'\0'
第一个 参数就是socket的fd,没啥说的。
第二个 参数是一个buf,可以理解为指针吧,指向一块空间,这里指向的是 char buffer[1024] 这块空间。
第三个 参数,就是你要从这块空间传入到缓冲区(对,重点,是传入到socket的发送缓冲区)多少字节,可以拿memcpy来理解,不就是字节的拷贝吗,当然还是有些不一样的。
记住 ,我们只需要知道send()/write() 是将数据包发送到缓冲区里就行了,至于是什么时候将缓冲区里的数据发送到接收端的socket就不用我们应用层来管了,实际上想管也管不到。
有两种情况会将缓冲区里的数据全部发送出去
情况1:缓冲区满了,很好理解吧,满了当然得发走啊,不然哪里有新的空间放新数据?
情况2:隔到一定时间,发现没有数据再继续send到缓冲区里来了,即便没有满也会赶紧发过去,而这个时间是非常短的。
所以,不用担心说数据发送不及时,而为什么要这样,则是为了提高数据发送的效率了。
char buffer[1024];
char buffer2[1024];
memset(buffer, 0, sizeof(buffer));
int len = read(fd, buffer, 5); //从读取缓冲区里读取5个字节
std::cout <<"len:" << len << " " << buffer << std::endl;
len = recv(fd, buffer2, 1024, 0); //从读取缓冲区里读取1024个字节(如果缓冲区里有1024数据的话)
std::cout <<"len:" << len << " " << buffer2 << std::endl;
close(fd); //关闭套接字
说起这个就来气,看了很多人写的文章,说什么read/recv 的3个参数是buffer的大小,即sizeof(buffer)
大哥们,长点心吧,这样很容易误导人的。不排除他们互相复制粘贴同一个人的。
第一个参数 就是socket的fd,还是一样没啥说的。
第二个参数 是一个buf,指向一块空间,可以理解为一个指针吧。就是说要把从缓冲区里读取到的数据放到这里来。
第三个参数 就是你要从缓冲区里读多少数据!!!如果缓冲区里没有那么多数据可读,就只会读取有的数据到buffer中,然后返回值是真正读取到的数据的字节数。
socket不论是发送还是接收缓冲区就相当于一个管道啊,先进先出,读取出来就不在管道里了。
我看过有文章介绍 recv()/read() 说这是用来从TCP的另一端来接收数据的,
这是不准确的,说明还没有理解透TCP的socket通信的原理,完全忽略了缓冲区的存在。
我们做一个实验就明白了,很简单,就是把上面服务器端的下面四句函数注释掉。
// int len = read(fd, buffer, 5); //从读取缓冲区里读取5个字节
// std::cout <<"len:" << len << " " << buffer << std::endl;
// len = recv(fd, buffer2, 1024, 0); //从读取缓冲区里读取1024个字节(如果缓冲区里有1024数据的话)
// std::cout <<"len:" << len << " " << buffer2 << std::endl;
然后运行,查看结果:
发现什么没有,receiver端即便没有调用recv()/read()函数,sender端还是发送成功了。
对的,在socket通信中,什么时候接收到数据,和如上面提到的,什么时候发送数据出去,都并不需要也不能由我们应用层来规定。
通俗的理解为,如果两个socket建立了连接,我们只需要send数据进发送缓冲区 和 从接收缓冲区里recv数据就行,真正的数据收发由内核来实施。
为什么这么说呢?
它是指TCP的数据传输就像一种水流一样,并不区分不同数据包之间的界限。就像我们打开水龙头后,水流自然的流出,我们并不知道背后水泵是分了几次将水供上来的。
如下面这个:
char buffer[1024]; //定义一个buf
strcpy(buffer,"hello world");
char buffer2[1024]; //定义一个buf2
strcpy(buffer,"I am happy");
send(sockfd, buffer, 11, 0); //发送"hello world",这里没有包括'\0'
send(sockfd, buffer, 11, 0); //发送"I am happy", 这里包括'\0'
根据上面讲到的缓冲区来,这个程序会怎么发送?
第一,两个数据包都很小,肯定都不会填满发送缓冲区,
第二,两个send之间没有时间延时,所以它们是合成一个数据包发送过去的。
因此,接收缓冲区里应该是两个"hello world"和"I am happy"粘在一起了,即
如果你直接像下面这样:
char buffer[1024]
int len = read(fd, buffer, sizeof(buffer)); //从缓冲区里读取1024个字节的数据
std::cout <<"len:" << len << " " << buffer << std::endl;
1024字节数据?基本就是把"hello world"和"I am happy"一起读取出来了,TCP并不会给两个不同的数据包分界线的,所以像流水一样,汇聚融合了。
所以接收缓冲区里面是这样的:hello worldI am happy0
只不过和流水不同的是,两个数据包之间还是有前后顺序存储在同一个接收缓冲区里的,不同的数据报文区之间就需要应用层人为去分开了。
而UDP就不同了,UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。
每个TCP socket在内核中都有一个发送缓冲区和一个接受缓冲区(前面详细介绍了)。TCP协议要求对端在接受到TCP数据报之后,要对其序号进行ACK,只有当接受到一个tcp数据报的ACK之后,才
可以把这个tcp数据报从socket的发送缓冲区清除(即发送数据过去),另外tcp还有一个流量控制功能,tcp的socket接受缓冲区接受到网络上来的数据缓存起来后,如果应用程序一直没有读取,
socket接受缓冲区满了之后,发生的动作是:通知对端TCP协议中的窗口关闭,这便是滑动窗口的实现,保证TCP socket接受缓冲区不会溢出,因为对方不允许发送超过所通
知窗口大小的数据, 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。这两点保证了tcp是可靠传输的。
UDP只有一个socket接受缓冲区,没有socket发送缓冲区,即只要有数据就发,不管对方是否可以正确接受。而在对方的socket接受缓冲区满了之后,新来的数据报无法进入到
socket接受缓冲区,此数据报就会被丢弃,udp是没有流量控制的,故UDP的数据传输是不可靠的。
总的来说吧,一个有问:“你的缓冲区有没有满啊?没满我就发数据过去了”
一个没有问就直接怼数据过去,然后缓冲区刚刚好一直没有被recv,所以满了,就丢弃在网络中了。
前面说那么多都是为了引出这个啊,这个是大头。
当然,一般的小打小闹不会有这个问题,当如果真正运用在了项目里,就不得不考虑这个问题了。
发现一个不错的文章,就懒得写了,自己看去:
https://blog.csdn.net/bjrxyz/article/details/73351248