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

一、前言

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

二、OSI简介

2.1 OSI概念

七层模型,亦称OSIOpen 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 <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
  1. socket()函数,为了执行网络输入输出,一个进程必须做的第一件事就是调用socket函数获得一个文件描述符。
/* Create a new socket of type TYPE in domain DOMAIN, using
   protocol PROTOCOL.  If PROTOCOL is zero, one is chosen automatically.
   Returns a file descriptor for the new socket, or -1 for errors.  */
extern int socket (int __domain, int __type, int __protocol) __THROW;
  • __domain为地址家族,一般填写AF_NET,表示互联网协议族(TCP/IP协议族),这里最常用的有AF_INET(IPv4协议)和AF_INET6(IPv6协议)
  • __type为类型,比如TCP则为SOCKET_STREMUDP则为SOCKET_DGRAM,以及SOCK_RAW(原始套接口)。
  • __protocol为协议编号,一般为0
  1. bind()函数,为套接口分配一个本地IP和协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合;如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。
/* Give the socket FD the local address ADDR (which is LEN bytes long).  */
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
     __THROW;
  • __fdsocket函数返回的套接口描述字;
  • __addr__len分别是一个指向特定于协议的地址结构的指针和该地址结构的长度。
  1. listen()函数:listen函数仅被TCP服务器调用,它的作用是将用socket创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。
/* Prepare to accept connections on socket FD.
   N connection requests will be queued before further requests are refused.
   Returns 0 on success, -1 for errors.  */
extern int listen (int __fd, int __n) __THROW;
  • __fdsocket函数返回的套接口描述字;
  • __n为可以建立连接的端口数;
  • 由于listen函数第二个参数的原因,内核要维护两个队列:已完成连接队列和未完成连接队列。未完成队列中存放的是TCP连接的三路握手为完成的连接,accept函数是从以连接队列中取连接返回给进程;当以连接队列为空时,进程将进入睡眠状态。
  1. accept()函数:accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。
/* Await a connection on socket FD.
   When a connection arrives, open a new socket to communicate with it,
   set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
   peer and *ADDR_LEN to the address's actual length, and return the
   new socket's descriptor, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int accept (int __fd, __SOCKADDR_ARG __addr,
		   socklen_t *__restrict __addr_len);
  • __fdsocket函数返回的套接口描述字;
  • __addr就是客户端的地址,这个是用来接受客户端的IP地址设置的,一旦存在客户端连接或者队列里面有,则获取它的IP地址,为后面需要使用。
  • __addr_len 是该客户端地址结构的长度;
  1. connect()函数,当用socket建立了套接口后,可以调用connect为这个套接字指明远程端的地址;如果是字节流套接口,connect就使用三次握手建立一个连接;如果是数据报套接口,connect仅指明远程端地址,而不向它发送任何数据。
/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
   For connectionless socket types, just set the default address to send to
   and the only address from which to accept transmissions.
   Return 0 on success, -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
  • __fdsocket函数返回的套接口描述字;
  • __addr是地址,可以是sockaddr_in或者sockaddr,其实最后都需要转化成sockaddr。
  • __len是地址的长度
  1. send()recv()函数,TCP套接字提供了send()recv()函数,用来发送和接收操作。这两个函数与write()read()函数很相似,只是多了一个附加的参数。
/* Send N bytes of BUF to socket FD.  Returns the number sent or -1.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);

/* Read N bytes into BUF from socket FD.
   Returns the number read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);

/* Send N bytes of BUF on socket FD to peer at address ADDR (which is
   ADDR_LEN bytes long).  Returns the number sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
  • __fd为套接字,不做解释了。
  • __buf为需要发送或者接受的数据,可以是char*
  • __n为需要发送或者接受的长度,为sizeof(buf)
  • __flag为传输控制标志,一般为0。
  1. close()函数,关闭套接字接口。
/* Close the file descriptor FD.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int close (int __fd);

3.1.3 服务端程序

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

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

#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)
    { /* calls listen() */
        perror("listen()error\n");
        exit(1);
    }
    else
    {
        printf("listen success!\n");
    }
    addrlen = sizeof(client);
    while (1)
    {
        if ((connectfd = accept(listenfd, (struct sockaddr *)&client, &addrlen)) == -1)
        {
            perror("accept()error\n");
            exit(1);
        }
        else
        {
            printf("accept success! connectfd: %d \n", connectfd);
        }
        printf("Yougot a connection from cient's ip is %s, prot is %d\n", 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>

#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 <IP Address>\n", argv[0]);
        exit(1);
    }
    if ((he = gethostbyname(argv[1])) == NULL)
    {
        printf("gethostbyname()error\n");
        exit(1);
    }
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("socket()error\n");
        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()error\n");
        exit(1);
    }

    while (1)
    {
        if ((num = recv(sockfd, buf, MAXDATASIZE, 0)) == -1)
        {
            printf("recv() error\n");
            exit(1);
        }
        else
        {
            printf("recv num: %d \n", num);
        }
        buf[num - 1] = '\0';
        printf("Server Message: %s\n", 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()函数中要指明目的地址。
/* Read N bytes into BUF through socket FD.
   If ADDR is not NULL, fill in *ADDR_LEN bytes of it with tha address of
   the sender, and store the actual size of the address in *ADDR_LEN.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
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()函数中要指明目的地址。
/* Send N bytes of BUF on socket FD to peer at address ADDR (which is
   ADDR_LEN bytes long).  Returns the number sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
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 <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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() error\n");
            exit(1);
        }

        buf[num] = '\0';
        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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#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 <IP Address><message>\n", argv[0]);
        exit(1);
    }

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

    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
    {
        printf("socket() error\n");
        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: %s\n", 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() error\n");
            exit(1);
        }
        printf("peer.sin_addr.s_addr: %s, peer.sin_port: %d\n", 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] = '\0';
        printf("Server Message:%s\n", 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端口。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值