TCP的socket详解

 


首先要看懂TCP的传输结构,至于那理想七层模型与实际的四层模型就不先说了,后面再补上。

我是提倡先会用,在会用的基础上去理解。

可以理解为TCP之间的数据传输都是依赖各自的socket,socket就充当传输的中介吧。

而每个socket都对应两个缓冲区,一个输入缓冲区,一个输出缓冲区 

怎么理解呢,且看下面的代码例子。


代码例子

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

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 <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#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()/write() 如上面用到的这个:

send(sockfd, buffer, 9, 0);        //发送9个字节,这里没有包括'\0'

第一个 参数就是socket的fd,没啥说的。

第二个 参数是一个buf,可以理解为指针吧,指向一块空间,这里指向的是 char buffer[1024] 这块空间。

第三个 参数,就是你要从这块空间传入到缓冲区(对,重点,是传入到socket的发送缓冲区)多少字节,可以拿memcpy来理解,不就是字节的拷贝吗,当然还是有些不一样的。

记住我们只需要知道send()/write() 是将数据包发送到缓冲区里就行了,至于是什么时候将缓冲区里的数据发送到接收端的socket就不用我们应用层来管了,实际上想管也管不到。

有两种情况会将缓冲区里的数据全部发送出去

情况1:缓冲区满了,很好理解吧,满了当然得发走啊,不然哪里有新的空间放新数据?

情况2:隔到一定时间,发现没有数据再继续send到缓冲区里来了,即便没有满也会赶紧发过去,而这个时间是非常短的。

所以,不用担心说数据发送不及时,而为什么要这样,则是为了提高数据发送的效率了。


接着就是 recv()/read()的了

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是面向流的协议?而UDP是面向数据报的协议?

为什么这么说呢?

它是指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可靠数据传输原理

每个TCP socket在内核中都有一个发送缓冲区和一个接受缓冲区(前面详细介绍了)。TCP协议要求对端在接受到TCP数据报之后,要对其序号进行ACK,只有当接受到一个tcp数据报的ACK之后,才

可以把这个tcp数据报从socket的发送缓冲区清除(即发送数据过去),另外tcp还有一个流量控制功能,tcp的socket接受缓冲区接受到网络上来的数据缓存起来后,如果应用程序一直没有读取,

socket接受缓冲区满了之后,发生的动作是:通知对端TCP协议中的窗口关闭,这便是滑动窗口的实现,保证TCP socket接受缓冲区不会溢出,因为对方不允许发送超过所通

知窗口大小的数据, 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。这两点保证了tcp是可靠传输的。

 

UDP不可靠数据传输原理

UDP只有一个socket接受缓冲区,没有socket发送缓冲区,即只要有数据就发,不管对方是否可以正确接受。而在对方的socket接受缓冲区满了之后,新来的数据报无法进入到

socket接受缓冲区,此数据报就会被丢弃,udp是没有流量控制的,故UDP的数据传输是不可靠的。

 

总的来说吧,一个有问:“你的缓冲区有没有满啊?没满我就发数据过去了”

一个没有问就直接怼数据过去,然后缓冲区刚刚好一直没有被recv,所以满了,就丢弃在网络中了。


粘包的处理

前面说那么多都是为了引出这个啊,这个是大头。

当然,一般的小打小闹不会有这个问题,当如果真正运用在了项目里,就不得不考虑这个问题了。

发现一个不错的文章,就懒得写了,自己看去:

https://blog.csdn.net/bjrxyz/article/details/73351248


 

  • 0
    点赞
  • 0
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值