分布式应用开发的核心技术系列之——基于TCP/IP的原始消息设计

2023-10-23 15:05:53 浏览数 (1)

本文由葡萄城技术团队原创并首发。转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。

前言

本文的内容主要围绕以下几个部分:

  1. TCP/IP的简单介绍。
  2. 消息的介绍。
  3. 基于消息分类的传输格式(流类型和XML类型)。
  4. 消息体系的组成。

TCP/IP的简单介绍

TCP/IP (传输控制协议/网际协议) 是互联网中的基本通信语言或协议。它其实是一个两层的程序,分为高层与低层。高层为传输控制协议,负责聚集信息或把文件拆分成更小的包。这些包通过网络传送到接收端的 TCP层,接收端的 TCP 层把包还原为原始文件。低层是网际协议,它处理每个包的地址部分,使这些包正确地到达目的地。网络上的网关计算机根据信息的地址来进行路由选择。即使来自同一文件的分包路由也有可能不同,但最后会在目的地汇合。TCP/IP 使用客户端/服务器模式进行通信。

在架构上,TCP/IP 并不完全符合 0SI 的 7 层参考模型。传统的开放式系统互连参考模型是一种通信协议的 7 层抽象的参考模型,其中每一层执行某一特定任务。该模型的目的是使各种硬件在相同的层次上相互通信。这 7 层是: 物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。而 TCP/IP 通信协议采用了 4 层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。这 4 层分别为:

  • 应用层:应用程序间沟通的层,如简单邮件传输协议 (SMTP)、文件传输协议 (FTP)、远程网络访问协议 (Telnet) 等。
  • 传输层:在此层中,它提供结点间的数据传送和应用程序之间的通信服务,主要功能是数据格式化、数据确认和丢失重传等。如传输控制协议 (TCP)、用户数据报协议 (UDP) 等,TCP 和 UDP 给数据包加入传输数据并把它传送到下一层中,这一层负责传送数据,并且确定数据已被送达并接收。
  • 互连网络层:负责提供基本的数据封包传送功能,让每一个数据包都能够到达目的主机 (但不检查是否被正确接收),如网际协议 (IP)。
  • 网络接口层 (主机-网络层): 接收 IP 数据报并进行传输,从网络上接收物理帧,抽取 IP 数据报转交给下一层,管理实际的网络媒体,定义如何使用实际网络 (如 Ethernet、Serial Line 等) 来传送数据。

Tcp/IP中常用的函数

1.Socket函数

代码语言:javascript复制
int socket(int domain,int type,int protocol),

domain 指明所使用的协议族,通常为 PF INET,表示互联网协议族(TCP/IP 协议族); type 参数指定 socket 的类型;用于 TCP 的SOCK STREAM 或用于 UDP 的 SOCK DGRAM; protocol 通常赋值[0]。socket函数调用返回一个整型 socket 描述符,可以在后面调用它。

2.bind函数:

bind 函数将 socket 与本机上的一个端口相关联,随后就可以在该端口监听服务请求。bind 函数原型为:

代码语言:javascript复制
int bind(int sockfd,struct sockaddr *my addr, int addrlen);

sockfd 是调用 socket 函数返回的 socket 描述符;my addr 是一个指向包含有本机 IP 地址及端口号等信息的 sockaddr 类型的指针:addrlen 常被设置为 sizeof (struct sockaddr)。

3.connect连接函数:

面向连接的客户程序使用连接 (connect) 函数来配置 socket 并与远端服务器建立一个 TCP 连接,其函数原型为:

代码语言:javascript复制
int connect(int sockfd, struct sockaddr *serv addr,int addrlen);

sockfd 是 socket 函数返回的 socket 描述符; serv addr 是包含远端主机 IP 地址和端口号的指针; addrlen 是远端地址结构的长度。connect 函数在出现错误时返回-1,并且设置 errno 为相应的错误码。进行客户端程序设计无须调用 bind 0,因为这种情况下只需要知道目的机器的 IP 地址即可,而客户通过哪个端口与服务器建立连接并不需要关心socket 执行体程序自动选择一个未被占用的端口,并通知程序数据什么时候到达端口。

4.listen监听函数:

网络监听 (listen) 函数使 socket 处于被动的监听模式,并为该socket 建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们。

代码语言:javascript复制
int listen(int sockfd, int backlog);

sockfd 是 Socket 系统调用返回的 socket 描述符;backlog 指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待接收函数accept 0)(参考下文)。backlog 对队列中等待服务的请求的数目进行了限制,通常系统默认值为 20。如果一个服务请求到来时,输入队列已满该 socket 将拒绝连接请求,客户将收到一个出错信息。

5.accept接收函数:

accept0函数让服务器接收客户的连接请求。在建立好输入队列后,服务器就调用 accept 函数,然后睡眠并等待客户的连接请求。

代码语言:javascript复制
int accept(int sockfd, void *addr, int *addrlen);

sockfd 是被监听的 socket 描述符,addr 通常是一个指向sockaddr_in 变量的指针,该变量用来存放提出连接请求服务的主机的信息(某台主机从某个端口发出该请求); addrlen 通常为一个指向值为sizeof (struct sockaddr in) 的整型指针变量。出现错误时 accept 函数返回-1 并设置相应的 errno 错误码。

6.sendto函数和recvfrom函数:

代码语言:javascript复制
int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen):

to 表示目的机的IP 地址和端号信息,而 tolen 常常被赋值为 sizeof (struct sockaddr)。sendto 函数返回实际发送的数据字节长度或在出现发送错误时返回-1。

代码语言:javascript复制
int recyfrom(int sockfd,void *buf,int len,unsigned int flags,structsockaddr *from,int *fromlen);

from 是一个 struct sockaddr 类型的变量,该变量保存源主机的 IP 地址及端口号。fromlen 常置为 sizeof (struct sockaddr),当 recvfrom()返回时,fromlen 包含实际存入 from 中的数据字节数。recvfrom() 函数返回接收到的字节数或当出现错误时返回-1,并设置相应的 errno 错误码。

7.shutdown函数

shutdown函数来关闭该 socket。该函数允许你只停止某个方向上的数据传输,而另一个方向上的数据传输继续进行。

代码语言:javascript复制
int shutdown(int sockfd,int how);

sockfd 是需要关闭的 socket 的描述符。参数 how 允许为 shutdown操作选择以下几种方式:

  • 0一一不允许继续接收数据
  • 1--不允许继续发送数据
  • 2一一不允许继续发送和接收数据

shutdown 在操作成功时返回 0,在出现错误时返回-1 并设置相应errno 错误码。

8.fcntl函数

fcntl函数可以改变已打开的文件的性质。

代码语言:javascript复制
int fcntl (int fields, int cmd, .../* int arg */) ;

9.getsockopt 与 setsockopt 函数

这两个函数可以获取或者设置与某个套接字关联的选项。为了操作套接字层的选项,应该将层的值指定为 SOL SOCKET。为了操作其他层的选项控制选项的合适协议号必须给出。例如,为了表示一个选项是由 TCP 解析,层应该设定为协议号 TCP。

代码语言:javascript复制
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);

10.select函数

select 函数是一种用于多路复用(Multiplexing)的系统调用或函数。它通常用于处理多个输入和输出流,以实现异步的 I/O 操作。

代码语言:javascript复制
int select(int n, fd set * readfds, fd set * writefds, fd set * exceptfds,struct timeval * timeout);

参数 n 代表最大的文件描述词加 1,参数 readfds、writefds 和exceptfds 称为描述词组,是用来回传该描述词的读、写或例外的状况。

11.poll函数

代码语言:javascript复制
int poll(struct pollfd fds[], nfds t nfds, int timeout);

其中 fds 是一个 struct pollfd 结构类型的数组,用于存放需要检测其状态的 socket 描述字。struct pollfd 的定义如下:

代码语言:javascript复制
struct pollfd {
        //descriptor to check
        int fd;
        //events of interest on fd
        short events;
        //events that occurred on fd
        short revents;
}

什么是消息

消息是分布式应用开发中,网络上两个逻辑实体之间进行通信时,在编程层面的最小单元。

对以上定义,有以下几点说明:

(1) 消息的概念存在于开发工作中,位于编程层面。在系统运行时,对应用用户是透明的。

(2) 网络上的两个逻辑实体,是指两个可独立运行的程序,它们可以部署于网络中两个不同的物理设备上,也可以部署于同一个物理设备上,但一般是两个没有父子关系的独立进程 (这一点与 IPC 编程中最基本的消息概念不同)。

(3) 消息是分布式通信时编程层面的最小单元,即无论参与通信的数据量是多还是少,程序代码中都通过发送与接收一个或多个消息来实现。

(4) 网络上两个应用之间的通信,包括数据流传输与远程过程(函数)调用两种类型。

(5) 利用消息可以实现分布式应用之间的结构化数据通信。也就是说编程人员在通信层面面对的不再是实际字节流,而是可以由多种数据类型组合而成的结构化数据单元。

其实,这种结构化数据单元本身就是“消息”,它对外可以表现为结构或者类。因此,当基于以上定义的消息机制建立起来以后,程序员在编码过程中,当需要进行分布式通信时,只需要生成相应的消息,然后调用相应的发送与接收接口方便地实现即可,而不需要了解 TCP/IP 知识,不需要掌握socket 编程的基本技能,也不需要考虑串行消息过多、并发消息过多、网络流量控制等其他多方面的问题,从而才能真正地将分布式应用开发的精力集中到业务实现上来,极大地提高了分布式系统的开发效率与质量,特别是大型分布式系统。

关于消息的存在形式,在传统 C 语言中,可以是一个结构 struct;在面向对象语言中 (C 或 Java),则可以是一个类 class。

基于消息分类的传输格式

基于消息传输的格式不同,可以将消息分为流消息和XML消息,流消息基于二进制字节流式格式传输,XML消息基于XML格式的字符串传输。

流消息

流消息是指在计算机系统中,以流(stream)的方式传递和处理的消息。流消息由一系列连续的数据组成,在发送端按照一定的顺序生成,并以流的形式传输到接收端。传输过程中,接收端可以逐个读取流中的数据。,对于流消息来说,无论程序员如何表示消息,消息在真正发送之前,都需要先转换为二进制流格式,这个转换过程称为流化 (Streamlization),也可称序列化 (Serilization),

XML消息

XML消息是指使用可扩展标记语言(XML)作为消息格式的数据传输方式。XML是一种用于描述和存储数据的文本标记语言,它使用标签来定义数据的结构和属性。在 XML 消息机制中,程序员用 XML 格式表示消息内容之后,不需要再为发送传输做任何格式转换工作(不包括为安全传输所做的加密工作),直接就可以以 XML 字符串格式发送出去。XML 消息应用也比较广泛,如 Web Service 中的 SOAP 协议,就是基于 XML 消息设计实现的。

举个例子:基于流消息的设计与实现方法

下面小编为大家简单地介绍一下如何在两个应用程序上发送和接受一个人的信息(包括身高、姓名和年龄)

(1)定义一个类存放人的信息:

代码语言:javascript复制
struct Person {
        char name[20] ;
        float height;
        int age;
}
struct Person p;
strcpy(p.name ,"Michael Zhang");
height = 170.00;
age = 30;

(2)将信息序列结构化

代码语言:javascript复制
char sendStream[1024] = {0};
sprintf(sendStream,"|%s|%f"%d",p.name, p.height, p.age);

(3)发送方发送字节流:

代码语言:javascript复制
/*注: 这里省略建立/管理/关闭 TCP 连接的代码*/
char datalen[4 1] = (0);
sprintf(datalen,"04d" , strlen (sendStream) );
if(SendBytes ( socket, datalen, 4) == -1) {
        return -l;
}
if(SendBytes(socket, sendStream, strlen(sendStream)) == -1) {
        return -1
}

注意,以上代码中的函数 SendBytes 实际上是保证一定长度的字节流全部成功发送完毕后才返回,主要是由于在 socket 上调用 send 或 write函数不能保证一次能将一定长度的字节流发送完。SendBytes 的基本思想是循环发送,直至成功发完所有字节,其实现代码如下所示:

代码语言:javascript复制
int SendBytes (int sd, const void *buffer, unsigned len) {
        int rez = 0;
        int leftlen = len;
        int readlen = 0:
}
while(true) {
        rez = write (socket, (char *)buffer readlen, len-readlen);
        if(rez < 0) {
                if (errno != EWOULDBLOCK && errno != EINTR) {
                        ErrorMsg("Error is serious );
                        DisConnect(socket);
        }
    return -l:
    }
    readlen  = rez;
    leftlen -= rez;
    if(leftlen <= 0){
    break;
    }
   }
return len:
}

(4)接收方接收字节流:

代码语言:javascript复制
char datalen[4 1] = {0};
char receiveStream[1024] = {0};
sprintf(datalen,"d", strlen(sendStream)) ;
if(ReceiveBytes(socket, datalen, 4) == -1 {
        return -l;
}
int packet len = atoi(datalen) :
if(ReceiveBytes (socket, receiveStream, packet len) == -1) {
        return -l;
}

ReceiveBytes函数可以参考第三步发送方发送该字节流。

(5)字节流反序列化得到结构:

代码语言:javascript复制
struct Person p;
sscanf(receiveStream,"%[`|]|%f|%d", p.name, &p.height, &p.age) ;

总结

本文简单的介绍了TCP/IP协议及其常用的接口函数,然后介绍了TCP/IP协议中消息的分类以及传输格式,最终以一个简单的消息发送小例子作为收尾。如对内容有何意见建议,欢迎大家在评论区中留言和讨论。

参考书籍:《消息设计与开发——分布式应用开发的核心技术》 何小朝

0 人点赞