栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

OSI七层模型以及TCP/UDP客户端/服务端程序实例

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

OSI七层模型以及TCP/UDP客户端/服务端程序实例

OSI七层模型以及TCP/UDP客户端/服务端程序实例
  • 一、前言
  • 二、OSI简介
    • 2.1 OSI概念
    • 2.2 划分原则
    • 2.3 OSI七层模型
    • 2.4 模型举例
  • 三、Linux下TCP/UDP程序开发
    • 3.1 TCP客户端/服务端开发
      • 3.1.1 通信流程
      • 3.1.2 函数简介
      • 3.1.3 服务端程序
      • 3.1.4 客户端程序
      • 3.1.5 通信测试
    • 3.2 UDP客户端/服务端开发
      • 3.2.1 通信流程
      • 3.2.2 函数简介
      • 3.2.3 服务端程序
      • 3.2.4 客户端程序
      • 3.2.5 通信测试

一、前言

准备写一篇文章分享TCP/UDP的客户端与服务端程序,想了一下,先介绍一下OSI七层模型的相关知识会比较好,这里参考大佬Java小白白又白的文章OSI七层模型及各层功能概述。

二、OSI简介 2.1 OSI概念

七层模型,亦称OSI(Open System Interconnection)。参考模型是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系,一般称为OSI参考模型或七层模型。
OSI参考模型定义了开放系统的层次结构和各层所提供的服务。OSI参考模型的一个成功之处在于,它清晰地分开了服务、接口和协议这3个容易混淆的概念。服务描述了每一层的功能,接口定义了某层提供的服务如何被高层访问,而协议是每一层功能的实现方法。通过区分这些抽象概念,OSI参考模型将功能定义与实现细节区分开来,概括性高,使它具有普遍的适应能力。

2.2 划分原则

OSI将整个通信功能划分为7个层次,划分的原则如下:

  1. 网络中所有节点都划分为相同的层次结构,每个相同的层次都有相同的功能。
  2. 同一节点内各相邻层次间可通过接口协议进行通信。
  3. 每一层使用下一层提供的服务,并向它的上层提供服务。
  4. 不同节点的同等层按照协议实现同等层之间的通信。

OSI模型具有如下优点:

  1. 分工合作,责任明确。性质相似的工作划分在同一层,性质不同的工作则划分到不同层,这样每一层的功能都是明确的,每一层都有其负责的工作范围,一旦出现问题,很容易找到问题所在的层,仅对此层加以改善即可;
  2. 对等交谈。计算机通过网络进行通信时,按照对等交谈的原则,即同一层找同层,通过各对等层的协议来进行通信,比如,两个对等的网络层使用网络协议通信;
  3. 逐层处理,层层负责。在OSI中,两个实体通信必须涉及下一层,只有相邻层之间可以通信,下层向上层提供服务,上层通过接口调用下层的服务,层间不能有越级调用关系,每层功能的实现都是在下层提供服务的基础上完成的。即每一层都是利用下层提供的服务来完成本层功能,并在此基础上为上层提供进一步的服务;
2.3 OSI七层模型



图片借鉴自:https://blog.csdn.net/yaopeng_2005/article/details/7064869
第一层:物理层
  在OSI参考模型中,物理层是参考模型的最低层,也是OSI模型的第一层。物理层的主要功能是:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输。物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异,使其上面的数据链路层不必考虑网络的具体传输介质是什么。

第二层:数据链路层
  数据链路层(Data Link Layer)是OSI模型的第二层,负责建立和管理节点间的链路。在计算机网络中由于各种干扰的存在,导致物理链路是不可靠的。因此这一层的主要功能是:在物理层提供的比特流的基础上,通过差错控制、流量控制方法,使有差错的物理线路变为无差错的数据链路,即提供可靠的通过物理介质传输数据的方法。

第三层:网络层
  网络层(Network Layer)是OSI模型的第三层,它是OSI参考模型中最复杂的一层,也是通信子网的最高一层,它在下两层的基础上向资源子网提供服务。其主要功能是:在数据链路层提供的两个相邻端点之间的数据帧的传送功能上,进一步管理网络中的数据通信,控制数据链路层与传输层之间的信息转发,建立、维持和终止网络的连接,将数据设法从源端经过若干个中间节点传送到目的端(点到点),从而向传输层提供最基本的端到端的数据传输服务。具体地说,数据链路层的数据在这一层被转换为数据包,然后通过路径选择、分段组合、顺序、进/出路由等控制,将信息从一个网络设备传送到另一个网络设备。数据链路层和网络层的区别为:数据链路层的目的是解决同一网络内节点之间的通信,而网络层主要解决不同子网间的通信。

第四层:传输层
  OSI下3层的任务是数据通信,上3层的任务是数据处理。而传输层(Transport Layer)是OSI模型的第4层。该层提供建立、维护和拆除传输连接的功能,起到承上启下的作用。该层的主要功能是:向用户提供可靠的端到端的差错和流量控制,保证报文的正确传输,同时向高层屏蔽下层数据通信的细节,即向用户透明地传送报文。

第五层:会话层
会话层是OSI模型的第5层,是用户应用程序和网络之间的接口,该层的主要功能是:组织和协调两个会话进程之间的通信 ,并对数据交换进行管理。当建立会话时,用户必须提供他们想要连接的远程地址。而这些地址与MAC地址或网络层的逻辑地址不同,它们是为用户专门设计的,更便于用户记忆。域名就是一种网络上使用的远程地址。会话层的具体功能如下:

  1. 会话管理:允许用户在两个实体设备之间建立、维持和终止会话,并支持它们之间的数据交换。
  2. 会话流量控制:提供会话流量控制和交叉会话功能。
  3. 寻址:使用远程地址建立会话连接。
  4. 出错控制:从逻辑上讲会话层主要负责数据交换的建立、保持和终止,但实际的工作却是接收来自传输层的数据,并负责纠正错误。

第六层:表示层
  表示层是OSI模型的第六层,它对来自应用层的命令和数据进行解释,对各种语法赋予相应的含义,并按照一定的格式传送给会话层。该层的主要功能是:处理用户信息的表示问题,如编码、数据格式转换和加密解密等。表示层的具体功能如下:

  1. 数据格式处理:协商和建立数据交换的格式,解决各应用程序之间在数据格式表示上的差异。
  2. 数据的编码:处理字符集和数字的转换。
  3. 压缩和解压缩:为了减少数据的传输量,这一层还负责数据的压缩与恢复。
  4. 数据的加密和解密:可以提高网络的安全性。

第七层:应用层
  应用层是OSI参考模型的最高层,它是计算机用户,以及各种应用程序和网络之间的接口,该层的主要功能是:直接向用户提供服务,完成用户希望在网络上完成的各种工作。它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外该层还负责协调各个应用程序间的工作。应用层的具体功能如下:

  1. 用户接口:应用层是用户与网络,以及应用程序与网络间的直接接口,使得用户能够与网络进行交互式联系。
  2. 实现各种服务:该层具有的各种应用程序可以完成和实现用户请求的各种服务。
2.4 模型举例

举例:以A公司向B公司发送一次商业报价单为例。

  • 应用层:A公司相当于实际的电脑用户,要发送的商业报价单相当于应用层提供的一种网络服务,当然A公司也可以选择其他服务,比如发一份商业合同,发一份询价单等等。

  • 表示层:由于A公司和B公司是不同国家的公司,他们之间商定统一用英语作为交流语言,所以此时A公司的文秘(表示层)将从上级手中(应用层)获取到的商业报价单的语言转翻译成英语,同时为了防止被别的公司盗取机密信息,A公司的文秘也会对这份报价单做一些加密的处理。这就是表示层的作用,将应用层的数据转换翻译。

  • 会话层:A公司外联部同事(会话层)掌握着其他许多公司的联系方式,他们负责管理本公司与外界许多公司的联系会话。当外联部同事拿到文秘(表示层)转换成英文的商业报价单后,他首先要找到B公司的地址信息,并附上自己的地址和联系方式,然后将整份资料放进信封准备寄出。等确认B公司接收到此报价单后,外联部的同事就去办其他的事情了,继而终止此次会话。

  • 传输层:传输层就相当于A公司中的负责收发快递邮件的人,A公司自己的投递员负责将上一层(会话层)要寄出的资料投递到快递公司或邮局。

  • 网络层:网络层就相当于快递公司庞大的快递网络,全国不同的集散中心,比如说从深圳发往北京的顺丰快递,首先要到顺丰的深圳集散中心,从深圳集散中心再送到武汉集散中心,从武汉集散中心再寄到北京顺义集散中心。这个每个集散中心,就相当于网络中的一个IP节点。

  • 数据链路层:相当于顺丰快递内部为了保证效率和质量的一种内部操作。

  • 物理层:快递寄送过程中的交通工具,就相当于物理层,例如汽车,火车,飞机,船。

三、Linux下TCP/UDP程序开发 3.1 TCP客户端/服务端开发 3.1.1 通信流程

3.1.2 函数简介

常用的头文件

#include 
#include 
#include 
#include 
  1. socket()函数,为了执行网络输入输出,一个进程必须做的第一件事就是调用socket函数获得一个文件描述符。
extern int socket (int __domain, int __type, int __protocol) __THROW;
  • __domain为地址家族,一般填写AF_NET,表示互联网协议族(TCP/IP协议族),这里最常用的有AF_INET(IPv4协议)和AF_INET6(IPv6协议)
  • __type为类型,比如TCP则为SOCKET_STREM,UDP则为SOCKET_DGRAM,以及SOCK_RAW(原始套接口)。
  • __protocol为协议编号,一般为0
  1. bind()函数,为套接口分配一个本地IP和协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合;如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
     __THROW;
  • __fd是socket函数返回的套接口描述字;
  • __addr和__len分别是一个指向特定于协议的地址结构的指针和该地址结构的长度。
  1. listen()函数:listen函数仅被TCP服务器调用,它的作用是将用socket创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。
extern int listen (int __fd, int __n) __THROW;
  • __fd是socket函数返回的套接口描述字;
  • __n为可以建立连接的端口数;
  • 由于listen函数第二个参数的原因,内核要维护两个队列:已完成连接队列和未完成连接队列。未完成队列中存放的是TCP连接的三路握手为完成的连接,accept函数是从以连接队列中取连接返回给进程;当以连接队列为空时,进程将进入睡眠状态。
  1. accept()函数:accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。
extern int accept (int __fd, __SOCKADDR_ARG __addr,
		   socklen_t *__restrict __addr_len);
  • __fd是socket函数返回的套接口描述字;
  • __addr就是客户端的地址,这个是用来接受客户端的IP地址设置的,一旦存在客户端连接或者队列里面有,则获取它的IP地址,为后面需要使用。
  • __addr_len 是该客户端地址结构的长度;
  1. connect()函数,当用socket建立了套接口后,可以调用connect为这个套接字指明远程端的地址;如果是字节流套接口,connect就使用三次握手建立一个连接;如果是数据报套接口,connect仅指明远程端地址,而不向它发送任何数据。
extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
  • __fd是socket函数返回的套接口描述字;
  • __addr是地址,可以是sockaddr_in或者sockaddr,其实最后都需要转化成sockaddr。
  • __len是地址的长度
  1. send()和recv()函数,TCP套接字提供了send()和recv()函数,用来发送和接收操作。这两个函数与write()和read()函数很相似,只是多了一个附加的参数。
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);


extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);


  • __fd为套接字,不做解释了。
  • __buf为需要发送或者接受的数据,可以是char*
  • __n为需要发送或者接受的长度,为sizeof(buf)
  • __flag为传输控制标志,一般为0。
  1. close()函数,关闭套接字接口。
extern int close (int __fd);
3.1.3 服务端程序

看完了函数介绍之后,我们就写一个简单的TCP服务端程序测试一下。
tcp_server_test.cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 1234
#define BACKLOG 1

int main()
{
    int listenfd, connectfd;
    struct sockaddr_in server;
    struct sockaddr_in client;
    socklen_t addrlen;
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("Creating  socket failed.");
        exit(1);
    }
    int opt = SO_REUSEADDR;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -1)
    {
        perror("Binderror.");
        exit(1);
    }

    if (listen(listenfd, BACKLOG) == -1)
    { 
        perror("listen()errorn");
        exit(1);
    }
    else
    {
        printf("listen success!n");
    }
    addrlen = sizeof(client);
    while (1)
    {
        if ((connectfd = accept(listenfd, (struct sockaddr *)&client, &addrlen)) == -1)
        {
            perror("accept()errorn");
            exit(1);
        }
        else
        {
            printf("accept success! connectfd: %d n", connectfd);
        }
        printf("Yougot a connection from cient's ip is %s, prot is %dn", inet_ntoa(client.sin_addr), htons(client.sin_port));
        char buf_send[100] = {0};
        sprintf(buf_send, "Welcometo my server, Port: %d.n", htons(client.sin_port));
        send(connectfd, buf_send, strlen(buf_send), 0);
        
    }
    close(connectfd);
    printf("close fd: %d n", connectfd);
    close(listenfd);
    return 0;
}
3.1.4 客户端程序

下面是TCP客户端程序示例,用来简单了解一下。
tcp_client_test.cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 1234
#define MAXDATASIZE 100

int main(int argc, char *argv[])
{
    int sockfd, num;
    char buf[MAXDATASIZE];
    struct hostent *he;
    struct sockaddr_in server;
    if (argc != 2)
    {
        printf("Usage:%s n", argv[0]);
        exit(1);
    }
    if ((he = gethostbyname(argv[1])) == NULL)
    {
        printf("gethostbyname()errorn");
        exit(1);
    }
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("socket()errorn");
        exit(1);
    }
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr = *((struct in_addr *)he->h_addr);
    if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
    {
        printf("connect()errorn");
        exit(1);
    }

    while (1)
    {
        if ((num = recv(sockfd, buf, MAXDATASIZE, 0)) == -1)
        {
            printf("recv() errorn");
            exit(1);
        }
        else
        {
            printf("recv num: %d n", num);
        }
        buf[num - 1] = '';
        printf("Server Message: %sn", buf);
        memset(buf, 0, MAXDATASIZE);
        sleep(1);
    }
    close(sockfd);
    return 0;
}
3.1.5 通信测试

编译程序进行测试

gcc -o tcp_client_test tcp_client_test.cpp
gcc -o tcp_server_test tcp_server_test.cpp


测试成功~

3.2 UDP客户端/服务端开发 3.2.1 通信流程

按照我之前测试的结果来看,UDP通信并不严格区分客户端与服务端,只是为了方便区别,客户端也可以添加bind()步骤。UDP对比TCP是无连接的通信,不需要listen()、accept()、connect()函数建立连接,可以直接使用recvfrom()、sendto()进行数据接收、发送

3.2.2 函数简介
  1. recvfrom函数:UDP使用recvfrom()函数接收数据,他类似于标准的read(),但是在recvfrom()函数中要指明目的地址。
extern ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n,
			 int __flags, __SOCKADDR_ARG __addr,
			 socklen_t *__restrict __addr_len);
  1. sendto函数:UDP使用sendto()函数发送数据,他类似于标准的write(),但是在sendto()函数中要指明目的地址。
extern ssize_t sendto (int __fd, const void *__buf, size_t __n,
		       int __flags, __CONST_SOCKADDR_ARG __addr,
		       socklen_t __addr_len);
3.2.3 服务端程序

udp_server_test.cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 1234
#define MAXDATASIZE 100

int main()
{
    int sockfd;
    struct sockaddr_in server;
    struct sockaddr_in client;
    socklen_t addrlen;
    int num;
    char buf[MAXDATASIZE];

    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
    {
        perror("Creatingsocket failed.");
        exit(1);
    }

    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
    {
        perror("Bind()error.");
        exit(1);
    }

    addrlen = sizeof(client);
    while (1)
    {
        num = recvfrom(sockfd, buf, MAXDATASIZE, 0, (struct sockaddr *)&client, &addrlen);

        if (num < 0)
        {
            perror("recvfrom() errorn");
            exit(1);
        }

        buf[num] = '';
        printf("You got a message (%s) from client.nIt's ip is: %s, port is %d.n", buf, inet_ntoa(client.sin_addr), ntohs(client.sin_port));
        sendto(sockfd, "Welcometo my server.n", 22, 0, (struct sockaddr *)&client, addrlen);
        if (!strcmp(buf, "bye"))
            break;
    }
    close(sockfd);
}
3.2.4 客户端程序

udp_client_test.cpp,这里需要注意的是,我的客户端也添加了bind()进行测试,目的是查看客户端能否使用使用指定端口,答案是肯定的,UDP客户端也可以bind(),证明了其实UDP没有所谓的客户端、服务端严格区分。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define PORT 1234
#define MAXDATASIZE 100

int main(int argc, char *argv[])
{
    int sockfd, num;
    char buf[MAXDATASIZE];

    struct hostent *he;
    struct sockaddr_in server, peer;

    if (argc != 3)
    {
        printf("Usage: %s n", argv[0]);
        exit(1);
    }

    if ((he = gethostbyname(argv[1])) == NULL)
    {
        printf("gethostbyname()errorn");
        exit(1);
    }

    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
    {
        printf("socket() errorn");
        exit(1);
    }
#ifndef DEBUG_BIND
    struct sockaddr_in client_bind;
    bzero(&client_bind, sizeof(client_bind));
    client_bind.sin_family = AF_INET;
    client_bind.sin_port = htons(4321);
    client_bind.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(sockfd, (struct sockaddr *)&client_bind, sizeof(client_bind)) == -1)
    {
        perror("Bind()error.");
        exit(1);
    }
#endif

    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr = *((struct in_addr *)he->h_addr);
    printf("server.sin_addr.s_addr: %sn", inet_ntoa(server.sin_addr));
    sendto(sockfd, argv[2], strlen(argv[2]), 0, (struct sockaddr *)&server, sizeof(server));
    socklen_t addrlen;
    addrlen = sizeof(server);
    while (1)
    {
        if ((num = recvfrom(sockfd, buf, MAXDATASIZE, 0, (struct sockaddr *)&peer, &addrlen)) == -1)
        {
            printf("recvfrom() errorn");
            exit(1);
        }
        printf("peer.sin_addr.s_addr: %s, peer.sin_port: %dn", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
        if (addrlen != sizeof(server) || memcmp((const void *)&server, (const void *)&peer, addrlen) != 0)
        {
            printf("Receive message from otherserver.n");
            continue;
        }

        buf[num] = '';
        printf("Server Message:%sn", buf);
        break;
    }

    close(sockfd);
}
3.2.5 通信测试

编译程序

gcc -o udp_server_test udp_server_test.cpp
gcc -o udp_client_test udp_client_test.cpp

这里我服务端使用1234端口发送消息,客户端使用4321端口。

暑期编程PK赛 得CSDN机械键盘等精美礼品!
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1017434.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号