c/c++基于liunx平台tcp协议通信分数据段发送小demo

引言*:tcp 是网络通信的一种协议,协议的话对于程序员来说就是按照约定的协议编写程序即可,具体编码的话相对于 之前做过的 socket 通信的 demo 来说 socket 通信来说并不会使用全新的API ,只是按照 tcp 协议按照要求 在发送消息的头部 加上相应的信息即可.

网络编程四层 TCP/ip

先 cp 一张大家都见过的图
在这里插入图片描述
OSI七层协议模型 (open system interconnection)
应用层————为应用数据提供服务
表示层————数据格式转化,数据加密
会话层————建立、维护和管理会话
传输层————建立、维护和管理端到端的链接,控制数据传输的方式
网络层————数据传输线路选择,IP地址及路由选择
数据链路层———物理通路的发送和数据包的划分,附加Mac地址到数据包
物理层———01比特流的转换
数据传输由顶向下,下层为上层提供服务

TCP/IP四层协议模型
应用层———负责处理特定的应用程序细节, 如ftp,http ,smtp,ssh 等
运输层———主要为两台主机上的应用提供端到端的通信, 如TCP,UDP。
网络层(互联网层)———处理分组在网络中的活动,比如分组的选路。
链路层(数据链路层/网络接口层)———包括操作系统中的设备驱动程序、计算机中对应的 网络接口卡,01比特流的转换

协议封装

下层协议通过封装为上层协议提供服务。应用程序数据在发送到物理网络上之前,将沿着协议栈从上往下依次传递。每层协议都将在上层数据的基础上加上自己的头部信息(有时也包括尾部信息),以实现该层的功能。
在这里插入图片描述

TCP协议头部

TCP 协议头部

源端口号和目的端口号:再加上Ip首部的源IP地址和目的IP地址可以唯一确定一个TCP连接
数据序号:表示在这个报文段中的第一个数据字节序号
确认序号:仅当ACK标志为1时有效。确认号表示期望收到的下一个字节的序号(这个下面再详细分析)
偏移:就是头部长度,有4位,跟IP头部一样,以4字节为单位。最大是60个字节
保留位:6位,必须为0
6个标志位:
URG-紧急指针有效
ACK-确认序号有效
PSH-接收方应尽快将这个报文交给应用层
RST-连接重置
SYN-同步序号用来发起一个连接
FIN-终止一个连接
窗口字段:16位,代表的是窗口的字节容量,也就是TCP的标准窗口最大为2^16 - 1 = 65535个字节
校验和:源机器基于数据内容计算一个数值,收信息机要与源机器数值 结果完全一样,从而证明数据的有效性。检验和覆盖了整个的TCP报文段:这是一个强制性的字段,一定是由发送端计算和存储,并由接收端进行验证的。
紧急指针:是一个正偏移量,与序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式
选项与填充(必须为4字节整数倍,不够补0):
最常见的可选字段的最长报文大小MSS(Maximum Segment Size),每个连接方通常都在一个报文段中指明这个选项。它指明本端所能接收的最大长度的报文段。
该选项如果不设置,默认为536(20+20+536=576字节的IP数据报)

三次握手

在这里插入图片描述
(1)男孩喜欢女孩,于是写了一封信告诉女孩:我喜欢你,请和我交往吧!写完信之后,男孩焦急地等待,因为不知道信能否顺利传达给女孩。
(2)女孩收到男孩的情书后,心花怒放,原来我们是两情相悦呀!于是给男孩写了一封回信:我收到你的情书了,也明白了你的心意,其实,我也喜欢你!我愿意和你交往!;
写完信之后,女孩也焦急地等待,因为不知道回信能否能顺利传达给男孩。
(3)男孩收到回信之后很开心,因为发出的情书女孩收到了,并且从回信中知道了女孩喜欢自己,并且愿意和自己交往。然后男孩又写了一封信告诉女孩:你的心意和信我都收到了,谢谢你,还有我爱你!
女孩收到男孩的回信之后,也很开心,因为发出的情书男孩收到了。由此男孩女孩双方都知道了彼此的心意,之后就快乐地交流起来了~~
所谓的三次握手即TCP连接的建立。这个连接必须是一方主动打开,另一方被动打开的。

四次挥手

在这里插入图片描述
1.客户端发送断开TCP连接请求的报文,其中报文中包含seq序列号,是由发送端随机生成的,并且还将报文中的FIN字段置为1,表示需要断开TCP连接。(FIN=1,seq=x,x由客户端随机生成)

2.服务端会回复客户端发送的TCP断开请求报文,其包含seq序列号,是由回复端随机生成的,而且会产生ACK字段,ACK字段数值是在客户端发过来的seq序列号基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP断开请求已经得到验证。(FIN=1,ACK=x+1,seq=y,y由服务端随机生成)

3.服务端在回复完客户端的TCP断开请求后,不会马上进行TCP连接的断开,服务端会先确保断开前,所有传输到A的数据是否已经传输完毕,一旦确认传输数据完毕,就会将回复报文的FIN字段置1,并且产生随机seq序列号。(FIN=1,ACK=x+1,seq=z,z由服务端随机生成)

4.客户端收到服务端的TCP断开请求后,会回复服务端的断开请求,包含随机生成的seq 字段和ACK字段,ACK字段会在服务端的TCP断开请求的seq基础上加1,从而完 成服务端请求的验证回复。(FIN=1,ACK=z+1,seq=h,h为客户端随机生成)

至此TCP断开的4次挥手过程完毕。

分包和粘包

TCP分包

场景:发送方发送字符串”helloworld”,接收方却分别接收到了两个数据包:字符串”hello”和”world”发送端发送了数量较多的数据,接收端读取数据时候数据分批到达,造成一次发送多次读取;
TCP是以段(Segment)为单位发送数据的,建立TCP链接后,有一个最大消息长度(MSS).如果应用层数据包超过MSS,就会把应用层数据包拆分,分成两个段来发送.

这个时候接收端的应用层就要拼接这两个TCP包,才能正确处理数据。
相关的,路由器有一个MTU( 最大传输单元)一般是1500字节,除去IP头部20字节,
留给TCP的就只有MTU-20字节。所以一般TCP的MSS为MTU-20=1460字节
当应用层数据超过1460字节时,TCP会分多个数据包来发送。

TCP 粘包

**场景:**发送方发送字符串”helloworld”,接收方却接收到了两个字符串”hello”和”world”

发送端发送了几次数据,接收端一次性读取了所有数据,造成多次发送一次读取;通常是网络流量优化,把多个小的数据段集满达到一定的数据量,从而减少网络链路中的传输次数
造成TCP粘包的原因:
TCP为了提高网络的利用率,会使用一个叫做Nagle的算法.该算法是指,发送端即使有要发送的数据,如果很少的话,会延迟发送.如果应用层给TCP传送数据很快的话,就会把两个应用层数据包“粘”在一起,TCP最后只发一个TCP数据包给接收端.

分包和粘包解决方案

发送数据前,给数据附加两字节的长度:

4字节 N个字节
FBEB 数据长度N 数据内容

  1. 包标识: 包头部的特殊标识,用来标识包的开始
  2. 数据长度:数据包的大小,固定长度,2、4 或者8字节。
  3. 数据内容:数据内容,长度为数据头定义的长度大小。

实际操作如下:
a)发送端:先发送包表示和长度,再发送数据内容。
b)接收端:先解析本次数据包的大小N,再读取N个字节,这N个字节就是一个完整的数据内容。

具体流程如下:
在这里插入图片描述

网络通信 tcp 协议分段发送测及其代码

本程序在前面的回声服务器的基础上加以改进加上了请求的头部分段发送数据

预期实现

服务器端启动后,等待客户端连接.
在这里插入图片描述
客户端通过 命令行的方式带入待发送的数据
服务器端 使用 tcp 协议 数据头部识别标识符定义为: fbeb 数据长度定义为 4 个字节
+信息
头部识别标识符+要发送数据长度+数据 客户端接收,分前 5 个数据和 第5个数据之后的数据
测试效果如下:
在这里插入图片描述

客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_PORT 8888
#define SERVER_IP  "127.0.0.1"
#define DATE_LEN_BYTES 4

const char* TRG = "febe";




int main(int argc, char *argv[]){

    int sockfd;
    char *message;
    struct sockaddr_in servaddr;
    int n;
    char * buf=NULL;

    if(argc != 2){
        fputs("Usage: ./echo_client message \n", stderr);
        exit(1);
    }

    message = argv[1];

    printf("message: %s\n", message);

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&servaddr, '\0', sizeof(struct sockaddr_in));

    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
    servaddr.sin_port = htons(SERVER_PORT);

    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

//    write(sockfd, message, strlen(message));
    //首先发送 TRG和DATE_LEN_BYTES

    int ms_len = strlen(message);
    int tag_len = strlen(TRG);
	  
    buf = (char*)malloc(ms_len+tag_len+DATE_LEN_BYTES);

    strcpy(buf,TRG);
	
    *(int*)(buf+tag_len) = ms_len;

    memcpy(buf+tag_len+DATE_LEN_BYTES,message,ms_len);
	
    write(sockfd,buf,tag_len+DATE_LEN_BYTES);
    write(sockfd,buf+tag_len+DATE_LEN_BYTES,5);
    sleep(2);	 
    write(sockfd,buf+tag_len+DATE_LEN_BYTES+5,ms_len-5);

    n = read(sockfd, buf,tag_len + DATE_LEN_BYTES + ms_len-1);

    if(n>0){
        buf[n]='\0';
        printf("receive: %s\n", buf);
    }else {
        perror("error!!!");
    }

    printf("finished.\n");
    close(sockfd);
}

服务器端代码

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include<stdlib.h>


#define SERVER_PORT 8888

#define DATE_LEN_BYTES 4

const char*TRG = "febe";

int read_len(int client_sock,char *buf,unsigned int len){

	int tag_len = strlen(TRG);
	//读包头
	int readlen = read(client_sock , buf,tag_len + DATE_LEN_BYTES);
	
	if(strncmp(buf,TRG,4)==0){//合法

	int date_len = *(int*)(buf+tag_len);
	
	
	int count = 0;

	while(count<date_len){

	
	readlen = read(client_sock,buf+count,date_len-count);
	printf("readlen:%d\n",readlen);

	count += readlen;

		}

	return date_len;

	}

	


}

int main(void){

    int sock;//代表信箱
    struct sockaddr_in server_addr;


    //1.美女创建信箱
    sock = socket(AF_INET, SOCK_STREAM, 0);

    //2.清空标签,写上地址和端口号
    bzero(&server_addr, sizeof(server_addr));

    server_addr.sin_family = AF_INET;//选择协议族IPV4
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本地所有IP地址
    server_addr.sin_port = htons(SERVER_PORT);//绑定端口号

    //实现标签贴到收信得信箱上
    bind(sock, (struct sockaddr *)&server_addr,  sizeof(server_addr));

    //把信箱挂置到传达室,这样,就可以接收信件了
    listen(sock, 128);

    //万事俱备,只等来信
    printf("等待客户端的连接\n");


    int done =1;

    while(done){
        struct sockaddr_in client;
        int client_sock, len, i;
        char client_ip[64];
        char buf[256];

        socklen_t  client_addr_len;
        client_addr_len = sizeof(client);
        client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);

        //打印客服端IP地址和端口号
        printf("client ip: %s\t port : %d\n",
                 inet_ntop(AF_INET, &client.sin_addr.s_addr,client_ip,sizeof(client_ip)),
                 ntohs(client.sin_port));
        /*读取客户端发送的数据*/
        //len = read(client_sock, buf, sizeof(buf)-1);
       len = read_len(client_sock,buf,sizeof(buf));

	buf[len] = '\0';
        printf("receive[%d]: %s\n", len, buf);

        //转换成大写
        for(i=0; i<len; i++){
            /*if(buf[i]>='a' && buf[i]<='z'){
                buf[i] = buf[i] - 32;
            }*/
            buf[i] = toupper(buf[i]);
        }


        len = write(client_sock, buf, len);

        printf("finished. len: %d\n", len);
        close(client_sock);

}
close(sock);
    return 0;
}

总结 使用 tcp 协议通信传输保证了通信传输的可靠性,但缺点是通信的流畅性并不能保证,它适用于对数据的可靠性要求高,但对流畅性要求并不高的场合.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值