文章目录
前言
一、网络知识概述
1. 网路结构分层
2. socket
3. IP地址
4. 端口号
5. 字节序
二、网络编程常用API
1. socket函数
2. bind函数
3. listen函数
4. accept函数
5. connect函数
6. read和recv函数
7. write和send函数
三、TCP编程
1. TCP介绍
2. TCP通信流程
3. TCP服务端和客户端程序
4. TCP并发服务器(多进程)
5. TCP并发服务器(多线程)
前言
记录学习嵌入式Linux网络编程的知识重点与难点,若涉及版权问题请联系本人删除!
一、网络知识概述
1. 网路结构分层
①主要存在两种分层模型:OSI七层模型(理论)和TCP/IP四层模型(实际)。
②分层思想:采用分治法,将复杂问题划分为各个层级上的子问题。每一层向上提供服务,同时使用下层提供的服务。
③网络封包和拆包:如下图所示,从主机A传输数据到主机B的流程一般为:主机A的应用层封装好了传输数据后,传输层会给上层数据包添加TCP头部,网络层会给上层数据包添加IP头部,网络接口层会给上层数据包添加对应的头部和尾部(一般为CRC校验);路由器将接收到的帧去掉头部和尾部传输给上一层,传输层将解析IP地址并查表转发,然后继续封装为帧,传输到指定的主机B;主机B逐层拆包,最终在应用层获取接收到的真正数据。
2. socket
①概念:socket是一个特殊的文件描述符,用于网络通信。
②socket分类:
- 流式套接字(SOCK_STREAM):对应TCP,面向连接、可靠。
- 数据报套接字(SOCK_DGRAM):对应UDP,无连接、不可靠。
- 原始套接字(SOCK_RAW):对应多个协议,可以直接访问IP、ICMP,跨过了传输层。
3. IP地址
①概念:IP地址是网络中主机地址的标识。通过IP可以找到对应的主机。
②IP地址分类:
- IPv4:一个32位(4字节)的整数,每个字节用.来分隔。每个字节的数据范围为0~255。例如: 192.168.5.11 不够用,引入局域网可以解决不够用的问题
- IPv6:一个128(16字节)的整数,每两个字节为一部分,总共有8部分,每部分用:分隔。例如:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b “地球上每一粒沙子都能分配一个”
③特殊的IP地址:
- 局域网IP:192.XXX.XXX.XXX 10.XXX.XXX.XXX
- 广播IP:XXX.XXX.XXX.255 255.255.255.255(全网广播)
- 组播IP:224.XXX.XXX.XXX到239.XXX.XXX.XXX
4. 端口号
①概念:是一个16位的整数,范围:1~65535.
②端口号分类:
- 众所周知的端口:1~1023. (FTP: 21, SSH: 22, HTTP: 80, HTTPS: 469)
- 保留端口:1024~5000 不建议使用
- 可以使用端口:5001~65535
③注意事项:
- TCP端口和UDP端口是相互独立的。(因为它们在内核中的处理路径不同)
- 网络通信由IP地址+端口号来决定。IP地址确定主机位置,端口号确定主机中哪个进程来处理。
5. 字节序
①概念:当涉及内存中存取多字节数据时,就会遇到字节序的问题。
②两种字节序:
- 小端:低字节序内容存储到低地址的内存中。
- 大端:低字节序内容存储到高地址的内存中。
③注意事项:
- 一般本地采用小端模式,网络传输采用大端模式。
- 在发送数据和接收数据前,应该先将本地字节序和网络字节序进行转换。
④主机字节序和网络字节序转换函数:
#include <arpa/inet.h>// 主要用于网络通信过程中IP和端口的转换
uint16_t htons(uint16_t hostshort); //短整形,主机字节序->网络字节序uint32_t htonl(uint32_t hostlong); //整形,主机字节序->网络字节序 uint16_t ntohs(uint16_t netshort); //短整形,网络字节序->主机字节序uint32_t ntohl(uint32_t netlong); //整形,网络字节序->主机字节序
⑤IP地址转换函数:
inet_pton函数:将IP地址从主机字节序->网络字节序。
【1】头文件:#include <arpa/inet.h>
【2】函数原型:int inet_pton(int af, const char *src, void *dst);
【3】参数说明:
- af:地址族,填写AF_INET(IPv4)或者AF_INET6(IPv6)
- src:传入参数,即要转换的IP地址。例如:192.168.5.11
- dst:传出参数,存放转换后的IP地址。
【4】返回值:成功返回1,第一个参数无效返回-1,第二个参数无效返回0.
inet_ntop函数:将IP地址从网络字节序->主机字节序。
【1】头文件:#include <arpa/inet.h>
【2】函数原型:const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
【3】参数说明:
- af:地址族,填写AF_INET(IPv4)或者AF_INET6(IPv6)
- src:传入参数,其中存储了网络字节序的IP地址。
- dst:传出参数,存放转换后的IP地址。
- size:dst指向的内存中最多可以存储多少个字节。
【4】返回值:成功返回指针指向第三个参数对应的地址,失败返回NULL。
二、网络编程常用API
以下函数除read和write外都需包含头文件#include <sys/types.h>、#include <sys/socket.h>
1. socket函数
【1】功能:创建网络套接字。
【2】函数原型:int socket(int domain, int type, int protocol);
【3】参数说明:
domain | AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 | |
AF_LOCAL | 本地通信 | |
type | SOCK_STREAM | 流式套接字,对应TCP |
SOCK_DGRAM | 数据报套接字,对应UDP | |
SOCK_RAW | 原始套接字 | |
protocol | 一般写0即可,使用默认协议。非0一般用于原始套接字。 |
【4】返回值:成功返回套接字fd,失败返回-1。
【5】代码示例:
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {perror("socket error");return -1;
}
2. bind函数
【1】功能:将本地的IP、端口与套接字绑定。
【2】函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
【3】参数说明:
sockfd | 套接字,由socket函数生成 | |
addr | struct sockaddr类型的变量的地址 | |
addrlen | addr指向的内存的地址大小 |
struct sockaddr结构体:(写数据时一般不用)
struct sockaddr {sa_family_t sa_family; // 地址族协议, ipv4char sa_data[14]; // 端口(2字节) + IP地址(4字节) + 填充(8字节) }
struct sockaddr_in结构体:(常用)一般将端口和IP地址保存在该类型的变量中,然后强转为struct sockaddr类型。(它们的大小完全相同)
struct sockaddr_in {sa_family_t sin_family; /* 地址族 */in_port_t sin_port; /* 端口号, 2字节 -> 网路字节序 */struct in_addr sin_addr; /* IP地址, 4字节 -> 网络字节序 *//* 填充8字节,初始化为0 */unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -sizeof (in_port_t) - sizeof (struct in_addr)]; };struct in_addr {in_addr_t s_addr; };
【4】返回值:成功返回0,失败返回-1。
【5】代码示例:
#define SERV_PORT 9999
#define SERV_IP "192.168.5.12"struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET; //IPv4协议
sin.sin_port = htons(SERV_PROT); //端口号,转化为网络字节序
//IP地址,转化为网络字节序
if (inet_pton(AF_INET, SERV_IP, &sin.sin_addr.s_addr) != 1) {perror("inet_pton error");return -1;
}
if (bind(fd, (struct sockaddr*)&sin, sizeof(sin))) {perror("bind error");return -1;
}
3. listen函数
【1】功能:设置套接字监听。调用之前需要bind绑定。
【2】函数原型:int listen(int sockfd, int backlog);
【3】参数说明:
sockfd | 监听的文件描述符,由socket函数生成 | |
backlog | 同时处理的最大连接数,一般可取5,最大值为128。表示系统允许2*backlog+1个客户端同时进行三次握手。 |
【4】返回值:成功返回0,失败返回-1。
【5】代码示例:
if (listen(fd, 128)) {perror("listen error");return -1;
}
4. accept函数
【1】功能:服务器阻塞等待客户端的连接请求,建立新连接,得到通信使用的套接字。
【2】函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
【3】参数说明:
sockfd | 监听的文件描述符,由socket函数生成 | |
addr | 传出参数,保存客户端的地址信息 | |
addrlen | addr指向的内存的大小 |
【4】返回值:成功返回与客户端进行通信的套接字,失败返回-1。
【5】代码示例:
struct sockaddr_in clientAddr;
int clientLen = sizeof(clientAddr);
int cfd = accept(fd, (struct sockaddr*)&clientAddr, &clientLen);
if (cfd < 0) {perror("accept error");return -1;
}
5. connect函数
【1】功能:客户端发起连接请求。成功连接服务器后,客户端会自动随机绑定一个端口。
【2】函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
【3】参数说明:
sockfd | 套接字,由socket函数生成 | |
addr | 存储要连接的服务端的IP和端口信息(需要网络字节序) | |
addrlen | addr指向的内存的大小 |
【4】返回值:成功返回0,失败返回-1。
【5】代码示例:
#define SERV_PORT 9999
#define SERV_IP "192.168.5.12"struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;//IPv4协议
sin.sin_port = htons(SERV_PORT);//目标服务端的端口号,网络字节序
//目标服务端的IP地址,网络字节序
if (inet_pton(AF_INET, SERV_IP, &sin.sin_addr.s_addr) != 1) {perror("inet_pton error");return -1;
}
//调用connect函数
if (connect(fd, (struct sockaddr*)&sin, sizeof(sin)) == -1) {perror("connect error");return -1;
}
6. read和recv函数
【1】功能:接收数据。
【2】函数原型:
- ssize_t read(int sockfd, void *buf, size_t len);
- ssize_t recv(int sockfd, void *buf, size_t len, int flags);
【3】参数说明:
sockfd | 用于通信的文件描述符 | |
buf | 存储接收的数据 | |
len | buf指向的内存的容量 | |
flags | 特殊属性,一般不使用,指定为0 |
【4】返回值:>0表示实际接收的字节数,==0表示对方断开了连接,-1表示失败。
7. write和send函数
【1】功能:接收数据。
【2】函数原型:
- ssize_t write(int sockfd, const void *buf, size_t len);
- ssize_t send(int sockfd, const void *buf, size_t len, int flags);
【3】参数说明:
sockfd | 用于通信的文件描述符 | |
buf | 发送的数据 | |
len | buf的长度 | |
flags | 特殊属性,一般不使用,指定为0 |
【4】返回值:>0表示实际发送的字节数(与len相等),-1表示失败。
三、TCP编程
1. TCP介绍
TCP(Transmission Control Protocol)是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:建立连接需要三次握手,断开连接需要四次挥手。
- 可靠安全:在TCP通信过程中,对每个发送的数据包都会进行校验,若数据丢失则重传。
- 流式传输:发送端和接收端的处理速度、数据量都可以不一致。
2. TCP通信流程
参考爱编程的大丙:
3. TCP服务端和客户端程序
服务端程序:接收客户端数据并打印,同时将收到的数据重新发回客户端。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>#define SERV_PORT 9999int main(int argc, char **argv)
{/* 1.创建TCP的套接字 */int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd == -1) {perror("server socket error");return -1;}/* 2.绑定IP和端口号 */struct sockaddr_in addr;memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET;addr.sin_port = htons(SERV_PORT);addr.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY宏表示本机所有IPint ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));if (ret == -1) {perror("server bind error");return -1;}/* 3.设置监听 */ret = listen(fd, 128);if (ret == -1) {perror("server listen error");return -1;}/* 4.等待客户端连接 */struct sockaddr_in clientAddr;int clientLen = sizeof(clientAddr);int cfd = accept(fd, (struct sockaddr*)&clientAddr, &clientLen);if (cfd == -1) {perror("server accept error");return -1;}//打印客户端信息char ip[24] = {0};printf("客户端IP: %s, 端口: %d\n",inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, ip, sizeof(ip)),ntohs(clientAddr.sin_port));/* 5.与客户端通信 */while (1) {//接收数据char buf[1024];memset(buf, 0, sizeof(buf));ret = read(cfd, buf, sizeof(buf));if (ret == 0) {printf("服务端:与客户端断开连接\n");break;} else if (ret < 0) {perror("server read error");break;} else {//打印接收数据,并重新发回接收的数据printf("服务端接收: %s\n", buf);write(cfd, buf, sizeof(buf));}}/* 6.关闭文件描述符 */close(cfd);close(fd);return 0;
}
客户端程序:每1秒发送指定数据,同时接收来自服务端的数据。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>#define SERV_PORT 9999
#define SERV_IP "192.168.124.6"int main(int argc, char **argv)
{/* 1.创建TCP的套接字 */int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd == -1) {perror("client socket error");return -1;}/* 2.与服务端建立连接 */struct sockaddr_in addr;memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET;addr.sin_port = htons(SERV_PORT);inet_pton(AF_INET, SERV_IP, &addr.sin_addr.s_addr);int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));if (ret == -1) {perror("client connect error");return -1;}//打印连接的服务端信息printf("连接的服务端IP: %s, 端口: %d\n", SERV_IP, SERV_PORT);/* 3.与服务端通信 */int number = 0;while (1) {//发送数据char buf[1024] = {0};sprintf(buf, "客户端: number = %d\n", number++);write(fd, buf, sizeof(buf));//接收数据memset(buf, 0, sizeof(buf));ret = read(fd, buf, sizeof(buf));if (ret == 0) {printf("客户端:与服务端断开连接\n");break;} else if (ret < 0) {perror("client read error");break;} else {printf("客户端接收: %s\n", buf);}sleep(1);//发送数据慢一些}/* 4.关闭文件描述符 */close(fd);return 0;
}
4. TCP并发服务器(多进程)
单个服务器需要能够与多个客户端进行通信,因此本节采用多进程方式来实现服务器的并发。
服务器中的父进程:
- 循环accept,每次与客户端建立新连接后,fork子进程。
- sigaction捕捉SIGCHLD信号,回收子进程资源。
服务器中的子进程:负责与新连接的客户端进行通信。
注意事项:accept函数阻塞时,若捕捉到SIGCHLD信号那么会取消阻塞并执行信号处理函数,那么处理完毕后accept函数返回值为-1并且错误号为EINTR。我们需要编写相关代码来处理这种情况,让服务器重新调用accept而不是退出。
服务器程序:监听所有的客户端连接,每次建立新连接后都创建子进程来与客户端进行通信。子进程读取从客户端接收的数据并显示。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>#define SERV_PORT 6666/* SIGCHLD处理函数 */
void cycle(int arg)
{while (1) {pid_t cPid = waitpid(-1, NULL, WNOHANG);if (cPid <= 0) {break;} else {printf("子进程%d被回收\n", cPid);}}
}/* 子进程与客户端通信 */
int serverCommu(int fd)
{char buf[1024] = {0};int len = read(fd, buf, sizeof(buf));if (len < 0) {printf("server read error\n");} else if (len == 0) {printf("服务端: 与客户端断开连接\n");} else {printf("服务端接收: %s\n", buf);}return len;
}int main(int argc, char **argv)
{/* 1.创建socket套接字 */int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) {perror("server socket error");return -1;}/* 2.绑定IP和端口 */struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(SERV_PORT);addr.sin_addr.s_addr = INADDR_ANY;int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));if (ret < 0) {perror("server bind error");return -1;}/* 3.设置监听 */ret = listen(fd, 5);if (ret < 0) {perror("server listen error");return -1;}/* 捕捉信号SIGCHLD, 回收子进程 */struct sigaction sact;sact.sa_flags = 0;sact.sa_handler = cycle;sigemptyset(&sact.sa_mask);sigaction(SIGCHLD, &sact, NULL);/* 4.循环等待客户端连接 */while (1) {//accept阻塞等待struct sockaddr_in clientAddr;int clientLen = sizeof(clientAddr);int cfd = accept(fd, (struct sockaddr*)&clientAddr, &clientLen);if (cfd == -1) {if (errno == EINTR) {//SIGCHLD处理后continue;}perror("server accept error");break;}//打印客户端IP和Portchar IPAddr[24];printf("连接的客户端IP: %s, 端口: %d\n",inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, IPAddr, sizeof(IPAddr)),ntohs(clientAddr.sin_port));//创建子进程与客户端通信pid_t pid = fork();if (pid < 0) {//出错perror("fork error");break;} else if (pid == 0) {//子进程close(fd);while (1) {int len = serverCommu(cfd);if (len <= 0) {break;}}close(cfd);return 0;} else {//父进程close(cfd);}}/* 5.父进程关闭套接字 */close(fd);return 0;
}
客户端程序:与服务器建立连接,发送用户输入的数据。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>#define SERV_PORT 6666
#define SERV_IP "192.168.5.12"int main(int argc, char **argv)
{/* 1.创建socket套接字*/int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) {perror("client socket error");return -1;}/* 2.建立连接 */struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(SERV_PORT);inet_pton(AF_INET, SERV_IP, &addr.sin_addr.s_addr);int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));if(ret < 0) {perror("client connect error");return -1;}/* 3.与服务端通信 */while (1) {//发送消息char buf[1024] = {0};char *tips = "客户端: ";int tipsLen = strlen(tips);strcpy(buf, tips);if (!fgets(buf+tipsLen, sizeof(buf)-tipsLen, stdin)) {perror("input error");continue;}write(fd, buf, sizeof(buf));}/* 4.关闭套接字 */close(fd);return 0;
}
测试结果:运行一个服务器程序、两个客户端程序,查看对应的结果。
5. TCP并发服务器(多线程)
本节采用多线程的方式来实现TCP并发服务器。
服务器中的主线程:
- 循环accept,将新建立的连接存储与某个结构体数组元素中。
- 每有一个客户端连接,就创建一个子线程来与其通信。
- 子线程分离,使子线程结束后让系统回收资源。
服务器中的子线程:负责与新连接的客户端进行通信。
注意事项:由于线程只有栈空间是独立的,需要注意数据覆盖的问题。在下面的程序案例中,采用一个全局的结构体数组来保存每一个线程对应操作的通信套接字、线程ID、客户端信息。
服务器程序:监听所有的客户端连接,在建立新连接前找到一块没有被占用的内存,保存建立新连接的通讯套接字、子线程ID和客服端信息。创建的子线程用来和客户端通信。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>
#include <string.h>#define SERV_PORT 6666typedef struct SockInfo {int fd; //通信的套接字pthread_t tid; //线程tidstruct sockaddr_in addr;//保存客户端信息
} SockInfo;SockInfo sInfos[128]; //每个线程对应一个结构体/* 子线程通信操作 */
void* working(void* arg)
{SockInfo *sinfo = (SockInfo*)arg;//打印客户端信息char IP[24] = {0};inet_ntop(AF_INET, &sinfo->addr.sin_addr.s_addr, IP, sizeof(IP));printf("连接的客户端IP: %s, 端口: %d\n", IP, ntohs(sinfo->addr.sin_port));//接收客户端的信息while (1) {char buf[1024] = {0};int ret = read(sinfo->fd, buf, sizeof(buf));if (ret < 0) {perror("server read error");break;} else if (ret == 0) {printf("服务端: 与客户端断开连接\n");break;} else {printf("服务端接收: %s\n", buf);}}//结束前清除数据sinfo->fd = -1;sinfo->tid = -1;memset(&sinfo->addr, 0, sizeof(sinfo->addr));return NULL;
}int main(int argc, char **argv)
{/* 1.创建socket套接字 */int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) {perror("server socket error");return -1;}/* 2.绑定IP和端口 */struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(SERV_PORT);addr.sin_addr.s_addr = INADDR_ANY;int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));if (ret < 0) {perror("server bind error");return -1;}/* 3.设置监听 */ret = listen(fd, 5);if (ret < 0) {perror("server listen error");return -1;}/* 4.主线程循环监听,子线程通信 */int sInfosLen = sizeof(sInfos) / sizeof(sInfos[0]);//初始化结构体数组for (int i = 0; i < sInfosLen; i++){sInfos[i].fd = -1;sInfos[i].tid = -1;memset(&sInfos[i].addr, 0, sizeof(sInfos[i].addr));}//循环监听while (1) {//遍历sInfos,找到未占用的元素SockInfo *pinfo = NULL;for (int i = 0; i < sInfosLen; i++){if (sInfos[i].fd == -1) {pinfo = &sInfos[i];break;}if (i == sInfosLen - 1) {//若全部被占用,停留在最后sleep(1);--i;}}//存在新连接,创建子线程,保存于pinfo指向的结构体int sockaddrLen = sizeof(pinfo->addr);int connfd = accept(fd, (struct sockaddr*)&pinfo->addr, &sockaddrLen);if (connfd < 0) {perror("server accept error");break;}pinfo->fd = connfd;pthread_create(&pinfo->tid, NULL, working, pinfo);pthread_detach(pinfo->tid);}/* 5.主线程关闭套接字 */close(fd);return 0;
}
客户端程序:与服务器建立连接,发送用户输入的数据。(与多进程的版本相同)
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>#define SERV_PORT 6666
#define SERV_IP "192.168.5.12"int main(int argc, char **argv)
{/* 1.创建socket套接字*/int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) {perror("client socket error");return -1;}/* 2.建立连接 */struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(SERV_PORT);inet_pton(AF_INET, SERV_IP, &addr.sin_addr.s_addr);int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));if(ret < 0) {perror("client connect error");return -1;}/* 3.与服务端通信 */while (1) {//发送消息char buf[1024] = {0};char *tips = "客户端: ";int tipsLen = strlen(tips);strcpy(buf, tips);if (!fgets(buf+tipsLen, sizeof(buf)-tipsLen, stdin)) {perror("input error");continue;}write(fd, buf, sizeof(buf));}/* 4.关闭套接字 */close(fd);return 0;
}
测试结果:运行一个服务器程序、两个客户端程序,查看对应的结果。