- 一、前言
- 二、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参考模型将功能定义与实现细节区分开来,概括性高,使它具有普遍的适应能力。
OSI将整个通信功能划分为7个层次,划分的原则如下:
- 网络中所有节点都划分为相同的层次结构,每个相同的层次都有相同的功能。
- 同一节点内各相邻层次间可通过接口协议进行通信。
- 每一层使用下一层提供的服务,并向它的上层提供服务。
- 不同节点的同等层按照协议实现同等层之间的通信。
OSI模型具有如下优点:
- 分工合作,责任明确。性质相似的工作划分在同一层,性质不同的工作则划分到不同层,这样每一层的功能都是明确的,每一层都有其负责的工作范围,一旦出现问题,很容易找到问题所在的层,仅对此层加以改善即可;
- 对等交谈。计算机通过网络进行通信时,按照对等交谈的原则,即同一层找同层,通过各对等层的协议来进行通信,比如,两个对等的网络层使用网络协议通信;
- 逐层处理,层层负责。在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地址或网络层的逻辑地址不同,它们是为用户专门设计的,更便于用户记忆。域名就是一种网络上使用的远程地址。会话层的具体功能如下:
- 会话管理:允许用户在两个实体设备之间建立、维持和终止会话,并支持它们之间的数据交换。
- 会话流量控制:提供会话流量控制和交叉会话功能。
- 寻址:使用远程地址建立会话连接。
- 出错控制:从逻辑上讲会话层主要负责数据交换的建立、保持和终止,但实际的工作却是接收来自传输层的数据,并负责纠正错误。
第六层:表示层
表示层是OSI模型的第六层,它对来自应用层的命令和数据进行解释,对各种语法赋予相应的含义,并按照一定的格式传送给会话层。该层的主要功能是:处理用户信息的表示问题,如编码、数据格式转换和加密解密等。表示层的具体功能如下:
- 数据格式处理:协商和建立数据交换的格式,解决各应用程序之间在数据格式表示上的差异。
- 数据的编码:处理字符集和数字的转换。
- 压缩和解压缩:为了减少数据的传输量,这一层还负责数据的压缩与恢复。
- 数据的加密和解密:可以提高网络的安全性。
第七层:应用层
应用层是OSI参考模型的最高层,它是计算机用户,以及各种应用程序和网络之间的接口,该层的主要功能是:直接向用户提供服务,完成用户希望在网络上完成的各种工作。它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外该层还负责协调各个应用程序间的工作。应用层的具体功能如下:
- 用户接口:应用层是用户与网络,以及应用程序与网络间的直接接口,使得用户能够与网络进行交互式联系。
- 实现各种服务:该层具有的各种应用程序可以完成和实现用户请求的各种服务。
举例:以A公司向B公司发送一次商业报价单为例。
-
应用层:A公司相当于实际的电脑用户,要发送的商业报价单相当于应用层提供的一种网络服务,当然A公司也可以选择其他服务,比如发一份商业合同,发一份询价单等等。
-
表示层:由于A公司和B公司是不同国家的公司,他们之间商定统一用英语作为交流语言,所以此时A公司的文秘(表示层)将从上级手中(应用层)获取到的商业报价单的语言转翻译成英语,同时为了防止被别的公司盗取机密信息,A公司的文秘也会对这份报价单做一些加密的处理。这就是表示层的作用,将应用层的数据转换翻译。
-
会话层:A公司外联部同事(会话层)掌握着其他许多公司的联系方式,他们负责管理本公司与外界许多公司的联系会话。当外联部同事拿到文秘(表示层)转换成英文的商业报价单后,他首先要找到B公司的地址信息,并附上自己的地址和联系方式,然后将整份资料放进信封准备寄出。等确认B公司接收到此报价单后,外联部的同事就去办其他的事情了,继而终止此次会话。
-
传输层:传输层就相当于A公司中的负责收发快递邮件的人,A公司自己的投递员负责将上一层(会话层)要寄出的资料投递到快递公司或邮局。
-
网络层:网络层就相当于快递公司庞大的快递网络,全国不同的集散中心,比如说从深圳发往北京的顺丰快递,首先要到顺丰的深圳集散中心,从深圳集散中心再送到武汉集散中心,从武汉集散中心再寄到北京顺义集散中心。这个每个集散中心,就相当于网络中的一个IP节点。
-
数据链路层:相当于顺丰快递内部为了保证效率和质量的一种内部操作。
-
物理层:快递寄送过程中的交通工具,就相当于物理层,例如汽车,火车,飞机,船。
常用的头文件
#include#include #include #include
- 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
- 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分别是一个指向特定于协议的地址结构的指针和该地址结构的长度。
- listen()函数:listen函数仅被TCP服务器调用,它的作用是将用socket创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。
extern int listen (int __fd, int __n) __THROW;
- __fd是socket函数返回的套接口描述字;
- __n为可以建立连接的端口数;
- 由于listen函数第二个参数的原因,内核要维护两个队列:已完成连接队列和未完成连接队列。未完成队列中存放的是TCP连接的三路握手为完成的连接,accept函数是从以连接队列中取连接返回给进程;当以连接队列为空时,进程将进入睡眠状态。
- accept()函数:accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。
extern int accept (int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len);
- __fd是socket函数返回的套接口描述字;
- __addr就是客户端的地址,这个是用来接受客户端的IP地址设置的,一旦存在客户端连接或者队列里面有,则获取它的IP地址,为后面需要使用。
- __addr_len 是该客户端地址结构的长度;
- 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是地址的长度
- 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。
- close()函数,关闭套接字接口。
extern int close (int __fd);3.1.3 服务端程序
看完了函数介绍之后,我们就写一个简单的TCP服务端程序测试一下。
tcp_server_test.cpp
#include3.1.4 客户端程序#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; }
下面是TCP客户端程序示例,用来简单了解一下。
tcp_client_test.cpp
#include3.1.5 通信测试#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; }
编译程序进行测试
gcc -o tcp_client_test tcp_client_test.cpp gcc -o tcp_server_test tcp_server_test.cpp
测试成功~
按照我之前测试的结果来看,UDP通信并不严格区分客户端与服务端,只是为了方便区别,客户端也可以添加bind()步骤。UDP对比TCP是无连接的通信,不需要listen()、accept()、connect()函数建立连接,可以直接使用recvfrom()、sendto()进行数据接收、发送
- 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);
- 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
#include3.2.4 客户端程序#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); }
udp_client_test.cpp,这里需要注意的是,我的客户端也添加了bind()进行测试,目的是查看客户端能否使用使用指定端口,答案是肯定的,UDP客户端也可以bind(),证明了其实UDP没有所谓的客户端、服务端严格区分。
#include3.2.5 通信测试#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); }
编译程序
gcc -o udp_server_test udp_server_test.cpp gcc -o udp_client_test udp_client_test.cpp
这里我服务端使用1234端口发送消息,客户端使用4321端口。



