前言
本人是一名大一学生,有幸被选拔进了深圳技术大学第一届开源鸿蒙菁英班,并在暑期培训进行线上分享,故将讲解的内容也制作成帖子发上来作为学习笔记。在准备分享的过程中,我基于学长们的先前成果,结合开源鸿蒙源码的最新版本进行了相应的调整和优化,帮助大家更好地理解和应用开源鸿蒙技术。本文旨在探讨TCP(Transmission Control Protocol,传输控制协议)通讯的相关知识。通过本文,您将了解TCP协议的工作原理,以及如何运用这一协议进行通讯程序设计与实现。
环境
- OpenHarmony - 4.0 源码
- 九联 unionpi_whale 开发板
一、TCP 通信介绍
1.概念
传输控制协议(TCP,Transmission Control Protocol)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。
2.特性
- OpenHarmony是一个分布式操作系统,它允许设备之间相互通信和协作。TCP是一种可靠的通信协议,适用于跨网络的设备间通信。通过实现TCP通信,设备可以安全、可靠地进行数据传输,实现各种协作功能。
- 广泛的支持:TCP是互联网上使用最广泛的通信协议之一,几乎所有的网络设备和操作系统都支持TCP协议。这意味着使用TCP作为通信协议可以提高OpenHarmony与其他设备和系统的兼容性,降低了集成和交互的复杂性。
- 成熟的实现和开发工具:TCP协议的实现和开发工具已经非常成熟,有许多可用的库和工具可以用于快速开发和部署TCP通信功能。这可以节省开发时间和资源,并且降低了开发过程中的风险。
- 支持面向连接的通信模式:TCP是一种面向连接的通信协议,它建立了可靠的双向通信通道,适合于需要长时间持续通信的场景,如客户端和服务器之间的通信。这种连接导向的通信模式可以满足许多应用场景的需求,包括实时数据传输、远程控制等。
- 通用性:TCP协议是传输层协议,与网络类型无关,因此它可以在各种类型的网络中使用,包括有线网络(如以太网)和无线网络(如Wi-Fi、蜂窝网络)。
3.两个重要概念:客户端与服务端
- 服务器被动连接,客户端主动连接:在TCP连接中,服务器通常处于被动状态,等待客户端的连接请求。而客户端则处于主动状态,负责发起连接请求。一旦连接建立成功,双方就可以进行数据传输。
4.指令认识
- 以下是几个网络调试常用的指令:
ifconfig # 用于Linux和OpenHarmony,常用于查看IP
ipconfig # 用于Windows,常用于查看IP
ping <IP> # 测试网络连通性
5.本节课使用工具 – NetAssist
- 一个网络调试工具:下载链接
二、Socket 编程(套接字编程)
1.socket()
socket() 函数是用于创建一个新的套接字(socket)的系统调用函数。套接字是一种通信机制,允许进程通过网络进行通信。在网络编程中,socket() 函数是一种创建套接字的标准方法,它通常在客户端和服务器端代码中都会用到。
- 函数原型:int socket(int domain, int type, int protocol);
- domain:指定套接字的地址族(Address Family),常见的包括:
- AF_INET:IPv4地址族
- AF_INET6:IPv6地址族
- AF_UNIX:UNIX本地域套接字
- type:指定套接字的类型,常见的包括:
- SOCK_STREAM:流套接字,用于面向连接的可靠数据传输,常用于TCP通信。
- SOCK_DGRAM:数据报套接字,用于无连接的不可靠数据传输,常用于UDP通信。
- protocol:指定协议,通常为0,表示使用默认协议。
2.close()
- close()函数用于关闭一个已经建立的TCP连接。关闭连接的目的是释放资源并告知对方连接的结束。
- 函数原型:close(int fd)
- fd:表示待绑定的套接字的文件描述符。
3.sockaddr_in
- sockaddr_in 结构体通常在网络编程中用于指定套接字的地址信息。
- 包含在头文件include <netinet/in.h>中
- 结构体成员:.
struct sockaddr_in{
sa_family_t sin_family; /* 指定地址族,即网络通信所使用的协议类型。*/
in_port_t sin_port; /* 表示端口号*/
struct in_addr sin_addr; /* 通配地址*/
unint8_t sin_zero[8]; /* 指定套接字的通信地址,从而确立通信的目标。通常未使用
};
- sin_addr 用于表示 IPv4 地址,可以是特定的 IP 地址,也可以是通配地址INADDR_ANY;
- sin_port 则表示端口号,用于标识一个网络服务。
- sin_family用于指定地址族,即网络通信所使用的协议类型。在IPv4的上下文中,它的值通常是AF_INET。
- sin_zero是一个长度为8的字节数组,通常未使用通过填充字段,可以指定套接字的通信地址,从而确立通信的目标。
- 与系统调用交互:在进行套接字编程时,常常需要将sockaddr_in结构体作为参数传递给一些系统调用函数,例如bind()、connect()、accept()等。这些函数通过读取sockaddr_in结构体中的地址信息,可以确定套接字的本地或远程地址,从而进行相应的操作,如绑定、连接或接受连接。
4.bind()
- bind() 函数是用于将一个套接字(socket)与一个特定的地址(通常是 IP 地址和端口号)绑定在一起的系统调用。在网络编程中,bind() 函数通常用于服务器端程序,在其创建套接字后,将该套接字绑定到一个特定的端口上,以便监听该端口并接受客户端的连接请求。
- 函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:指定要绑定的套接字文件描述符。
- addr:指向 sockaddr 结构体的指针,该结构体包含了要绑定的地址信息。
- addrlen:指定地址结构体的长度。
- 如果绑定成功,bind() 函数返回 0,否则返回 -1。
- 示例:
if (bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) == -1)
{
perror("Error binding socket");
close(sockfd);
}
5.listen()
- listen() 函数用于将指定的套接字设置为监听状态,开始接受客户端的连接请求。在服务器端编程中,通常在调用 bind() 函数绑定地址之后,使用 listen() 函数来准备套接字接受连接请求。
- 函数原型:int listen(int sockfd, in MAX_CLIENTS);
- sockfd:表示待绑定的套接字的文件描述符,即通过 socket() 函数创建的套接字。
- MAX_CLIENTS:参数指定了内核允许在套接字队列中等待的连接的最大数量。如果队列已满,后续的连接请求将被拒绝,直到有连接被接受或队列中的连接被处理。这个参数通常设置为一个适当的值,以确保服务器能够处理所有传入的连接请求。
- 如果绑定成功,listen() 函数返回 0,否则返回 -1。
- 示例:
if (listen(server_sock, MAX_CLIENTS) == -1)
{
perror("Failed to listen");
close(server_sock);
}
6.accept()
- accept() 函数是在服务器端套接字上调用的系统调用,用于接受客户端的连接请求,并创建一个新的套接字用于与客户端进行通信。这个函数通常在调用了 listen() 函数之后,用于实际接受传入的连接。
- 函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)。
- sockfd:指定要接受连接请求的监听套接字文件描述符。
- addr:用于存储客户端地址信息的结构体指针。如果不需要获取客户端地址信息,可以传入 NULL。
- addrlen:指向一个整数的指针,表示客户端地址信息结构体的长度。在调用 accept() 函数之前,应该将其初始化为 sizeof(struct sockaddr)。
- 当调用 accept() 函数时,它会阻塞程序的执行,直到有客户端连接请求到达服务器套接字 sockfd。一旦有连接请求到达,accept() 函数会从服务器的待处理连接队列中取出一个连接请求,并创建一个新的套接字来处理该连接。这个新的套接字将用于与客户端进行通信,而服务器原始的套接字继续监听其他连接请求。
- 如果绑定成功,bind() 函数返回 0,否则返回 -1。
- 示例:
if (accept(sockfd,(struct sockaddr *)&client_addr,&client_len) == -1)
{
perror("Error accepting connection");
close(sockfd);
}
7.connect()
- connect() 函数用于客户端套接字向服务器发起连接请求。当客户端需要与远程服务器建立连接时,就可以使用 connect() 函数。
- 函数原型: connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:表示客户端套接字的文件描述符。
- addr:是一个指向 sockaddr 结构体(或其子结构体,如 sockaddr_in)的指针,其中包含要连接的服务器地址信息。
- addrlen:表示地址结构体的长度。
- 如果连接请求成功建立,则 connect() 函数返回 0,并客户端套接字就可以开始与服务器进行通信了,如果连接请求失败,则 connect() 函数返回 -1。
- 在 connect() 函数调用期间,通常会发生阻塞。这意味着当 connect() 函数在建立连接时,程序会暂停执行,直到连接成功建立或者发生错误。
- 需要注意的是,connect() 函数只能在套接字类型为流套接字(如 SOCK_STREAM)的情况下使用,因为它是用于建立可靠的、面向连接的连接。对于数据报套接字(如 SOCK_DGRAM),应该使用 sendto() 函数进行发送,而不是 connect()。
- 示例:
if (connect(client_sock,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
{
perror("Failed to connect to server");
}
8.recv() 与 read()
在TCP通信中,recv和read函数都是用来从socket接收数据的,但它们在不同的编程语言和平台上有一些细微的区别。
(1)recv()
- 函数原型:recv(int sockfd, void *buf, size_t len, int flags);
- recv是通用的socket接收函数,在许多编程语言和操作系统中都有实现。它的作用是从已连接的socket中接收数据,并将接收到的数据存储到指定的缓冲区中。
- recv函数通常可以设置一些参数,比如要接收的最大字节数、接收数据的起始位置等。
- 其中sockfd是socket文件描述符,buf是接收数据的缓冲区,len是要接收的最大字节数,flags是一些控制接收行为的选项。
(2)read()
- 函数原型:read(int fd, void *buf, size_t count);
- read函数在OpenHarmony系统中用于从文件描述符(包括socket)中读取数据。它的作用也是从socket接收数据,类似于recv,但是read函数更多地用于文件I/O。
- read函数的用法与recv类似,也需要指定接收数据的缓冲区和最大字节数。
- buf是接收数据的缓冲区,count是要读取的最大字节数。
9.send() 与 write()
在TCP通信中,send()和write()函数在TCP通信中都起着发送数据的作用。
(1)send()
- 函数原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- send函数是通用的socket发送函数,在许多编程语言和操作系统中都有实现,它的作用是将数据从指定的缓冲区发送到已连接的socket 。
- send()函数通常可以设置一些参数,比如要发送的数据长度、发送数据的起始位置等。
- sockfd是套接字文件描述符,buf是要发送数据的缓冲区,len是要发送的数据长度,flags是一些控制发送行为的选项。如果不需要特定的控制选项,可以将flags参数设置为0,以便使用默认的行为。
(2)write()
- 函数原型:ssize_t write(int fd, const void *buf, size_t count);
- write函数在OpenHarmony系统中用于向文件描述符(包括socket)写入数据。它的作用也是将数据从指定的缓冲区发送到文件描述符所代表的对象(可能是socket)。
- write函数的用法与send类似,也需要指定要发送的数据的缓冲区和数据长度。
- fd是文件描述符,可以是socket文件描述符,buf是要发送数据的缓冲区,count是要发送的数据长度。
10.网络编程中可能用到的几个函数
- htons()函数:用于将一个 16 位的无符号短整型数据从主机字节序转换为网络字节序。在网络通信中,不同的计算机可能具有不同的字节序,为了确保数据在网络中的正确传输,需要进行字节序的转换。例如,如果主机是小端字节序,而网络使用大端字节序,那么通过 htons()可以将主机上存储的短整型数据转换为适合在网络上传输的大端字节序形式。
- inet_ntoa()函数:用于将一个 32 位的网络字节序的 IPv4 地址转换为点分十进制的字符串形式。例如,如果有一个 IPv4 地址以网络字节序存储在一个 in_addr 结构体中,可以使用 inet_ntoa() 将其转换为人们常见的点分十进制表示,如 “192.168.0.1” 。
- ntohs()函数:功能与 htons() 相反,它将一个 16 位的网络字节序无符号短整型数据转换为主机字节序。
- inet_pton()函数:用于将一个点分十进制表示的 IPv4 或 IPv6 地址转换为网络字节序的二进制形式,并存储在指定的地址结构中。举例来说,如果你在使用C语言进行网络编程,你可能会在调用connect()函数连接到远程服务器之前,需要将字符串形式的IP地址转换为套接字库可以理解的形式,这时就需要使用inet_pton()函数。
三、TCP通信实例
本文文件结构如下:
在OpenHarmony源码根目录下创建文件夹Mysample,下创文件夹tcp_demo:
1.服务端实例
(1)实现流程
- 初始化:
创建一个socket对象,通常使用socket(AF_INET, SOCK_STREAM, 0)创建一个面向连接的socket。设置socket的端点信息,包括IP地址和端口号,通常使用bind()函数。启动监听,使用listen()函数设置监听队列大小。 - 等待连接:使用accept()函数等待客户端连接。accept()会返回一个新的socket对象,用于与客户端进行通信。
- 接收数据:使用read()函数接收客户端发送的数据。
- 发送数据:使用send()函数将数据发送给客户端。
- 关闭连接:完成数据交换后,使用close()函数关闭socket,释放资源。
(2)代码实现
"Mysample\tcp_demo\src\tcp_demo.cpp"
#include <iostream> // 引入标准输入输出流库
#include <stdio.h> // 引入标准输入输出库
#include <string.h> // 引入字符串处理库
#include <netinet/in.h> // 引入IP网络库
#include <arpa/inet.h> // 引入地址转换库
#include <sys/socket.h> // 引入套接字库
#include <unistd.h> // 引入Unix系统调用库
// 服务器端口号
#define SERVER_PORT 4567
// 最大客户端数量
#define MAX_CLIENTS 5
// 缓冲区大小
#define TCP_BUFFER_SIZE 1024
int main()
{
// 创建服务器套接字和客户端套接字
int server_sock, client_sock;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 创建一个服务端TCP套接字
server_sock = socket(AF_INET, SOCK_STREAM, 0);
if(server_sock == -1)
{
perror("Failed to create socket"); // 创建套接字失败,打印错误信息
exit(EXIT_FAILURE); // 退出程序
}
// 设置服务端地址
server_addr.sin_family = AF_INET; // 设置地址族为IPv4
server_addr.sin_port = htons(SERVER_PORT); // 设置端口号,htons确保端口号为网络字节序
server_addr.sin_addr.s_addr = INADDR_ANY; // 设置IP地址为INADDR_ANY,表示接受任何接口的连接
// 绑定套接字
if (bind(server_sock, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1)
{
perror("Failed to bind socket"); // 绑定套接字失败,打印错误信息
close(server_sock); // 关闭套接字
exit(EXIT_FAILURE); // 退出程序
}
// 开始监听客户端连接请求
if (listen(server_sock, MAX_CLIENTS) == -1)
{
perror("Failed to listen"); // 监听失败,打印错误信息
close(server_sock); // 关闭套接字
exit(EXIT_FAILURE); // 退出程序
}
std::cout << "Server is listening on port " << SERVER_PORT << std::endl; // 打印服务器监听端口信息
// 主循环,等待客户端连接
while(true)
{
// 接受客户端连接请求
client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_sock == -1)
{
perror("Failed to accept connection"); // 接受连接失败,打印错误信息
continue; // 继续下一次循环
}
// 打印出连接成功的客户端的IP地址和端口号
std::cout << "Accepted connection from " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl;
// 发送数据给客户端
char request[] = "Hello, here is server!"; // 定义要发送的字符串
size_t bytes_write = write(client_sock, request, strlen(request)); // 发送数据,并返回发送的字节数
if (bytes_write == -1)
{
perror("Failed to write data"); // 发送数据失败,打印错误信息
close(client_sock); // 关闭客户端套接字
exit(EXIT_FAILURE); // 退出程序
}
// 接受客户端发送的数据
char buffer[TCP_BUFFER_SIZE]; // 定义缓冲区用于接收数据
size_t bytes_read = read(client_sock, buffer, TCP_BUFFER_SIZE); // 从客户端读取数据,并返回读取的字节数
if (bytes_read == -1)
{
perror("Failed to read data"); // 读取数据失败,打印错误信息
close(client_sock); // 关闭客户端套接字
exit(EXIT_FAILURE); // 退出程序
}
else
{
std::cout << "Received data from client: " << buffer << std::endl; // 打印接收到的数据
break; // 退出循环
}
}
// 关闭服务器套接字
close(server_sock);
return 0;
}
2.客户端实例
(1)实现流程
- 创建套接字:使用socket函数创建一个TCP套接字。参数通常包括地址族(AF_INET表示IPv4)、套接字类型(SOCK_STREAM表示TCP流式套接字)和协议(通常为0,表示使用默认的TCP协议)。
- 设置服务器地址:创建一个sockaddr_in结构体,用于存储服务器的IP地址和端口号。
- 连接到服务器:使用connect函数将客户端套接字连接到服务器。
- 发送数据:使用write系统函数调用来发送数据到服务器。
- 接收数据:使用read系统函数调用来从服务器接收数据。
- 关闭连接:当数据传输完成后,使用close函数关闭套接字。
(2)代码实现
"Mysample\tcp_demo\src\tcp_demo.cpp"
#include <iostream> // 引入输入输出流库
#include <stdio.h> // 引入标准输入输出库
#include <string.h> // 引入字符串处理库
#include <cstring> // 引入C风格字符串处理库
#include <sys/socket.h> // 引入socket编程库
#include <netinet/in.h> // 引入网络地址结构定义库
#include <arpa/inet.h> // 引入网络地址转换库
#include <unistd.h> // 引入Unix标准库,提供close函数
#define TCP_BUFFER_SIZE 1024 // 定义TCP缓冲区大小为1024字节
int main()
{
int client_sock = socket(AF_INET, SOCK_STREAM, 0); // 创建一个IPv4的TCP socket
if (client_sock == -1) // 如果socket创建失败
{
perror("Failed to create socket"); // 输出错误信息
exit(EXIT_FAILURE); // 退出程序
}
struct sockaddr_in server_addr; // 创建一个服务器地址结构体
int SERVER_PORT; // 服务器端口变量
std::string SERVER_ADDR; // 服务器地址变量
std::cout << "Input server address: "; // 输出提示信息
std::cin >> SERVER_ADDR; // 从标准输入读取服务器地址
std::cout << "Input server port: "; // 输出提示信息
std::cin >> SERVER_PORT; // 从标准输入读取服务器端口
server_addr.sin_family = AF_INET; // 设置地址族为IPv4
server_addr.sin_port = htons(SERVER_PORT); // 设置端口,网络字节序
if (inet_pton(AF_INET, SERVER_ADDR.c_str(), &server_addr.sin_addr) <= 0) // 如果地址转换失败
{
std::cerr << "Invalid address/ Address not supported" << std::endl; // 输出错误信息
return -1; // 返回错误
}
while (true) // 无限循环尝试连接
{
if (connect(client_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) // 如果连接失败
{
perror("Failed to connect to server"); // 输出错误信息
close(client_sock); // 关闭socket
exit(EXIT_FAILURE); // 退出程序
}
std::cout << "Connected to server " << inet_ntoa(server_addr.sin_addr) << ":" << ntohs(server_addr.sin_port) << std::endl; // 输出连接成功信息
// 向服务器发送数据
char request[] = "Hello, here is client!"; // 创建请求字符串
size_t bytes_write = write(client_sock, request, strlen(request)); // 发送数据
if (bytes_write == -1) // 如果发送失败
{
perror("Failed to write data"); // 输出错误信息
close(client_sock); // 关闭socket
exit(EXIT_FAILURE); // 退出程序
}
// 从服务器接收数据
char buffer[TCP_BUFFER_SIZE]; // 创建缓冲区
size_t bytes_read = read(client_sock, buffer, TCP_BUFFER_SIZE); // 读取数据
if (bytes_read == -1) // 如果接收失败
{
perror("Failed to read data"); // 输出错误信息
close(client_sock); // 关闭socket
exit(EXIT_FAILURE); // 退出程序
}
else // 如果接收成功
{
std::cout << "Received data from server: " << buffer << std::endl; // 输出接收到的数据
break; // 退出循环
}
}
// 关闭
close(client_sock); // 关闭socket
return 0; // 程序成功结束
}
3.编译构建文件
"C:\Users\LIGANG\Desktop\Mysample\tcp_demo\BUILD.gn"
import("//build/ohos.gni") # 导入编译模板
ohos_executable("tcp") { # 可执行模块
sources = [ # 模块源码
"src/tcp_demo.cpp"
]
cflags = []
cflags_c = []
cflags_cc = []
ldflags = []
configs = []
deps =[] # 部件内部依赖
part_name = "tcp_demo" # 所属部件名称,必选
install_enable = true # 是否默认安装(缺省默认不安装),可选
}
"C:\Users\LIGANG\Desktop\Mysample\tcp_demo\bundle.json"
{
"name": "@ohos/tcp_demo",
"description": "",
"version": "3.1",
"license": "Apache License 2.0",
"publishAs": "code-segment",
"segment": {
"destPath": "Mysample/tcp_demo"
},
"dirs": {},
"scripts": {},
"component": {
"name": "tcp_demo",
"subsystem": "Mysample",
"syscap": [],
"features": [],
"adapted_system_type": [
"standard"
],
"rom": "10KB",
"ram": "10KB",
"deps": {
"components": [],
"third_party": []
},
"build": {
"sub_component": [
"//Mysample/tcp_demo:tcp"
],
"inner_kits": [],
"test": []
}
}
}
4.编译
- 命令行方式
./build.sh --product-name {product_name} #全量编译
./build.sh --product-name {product_name} --build-target {target_name} #单独编译部件
./build.sh --product-name {product_name} --build-target {target_name} --fast-rebuild #快速重建
- hb方式
hb set #设置编译参数
hb build #全量编译
hb build -T {target_name} #单独编译部件
hb build -T {target_name} --fast-rebuild #快速重建
- 我们这里使用hb方式来进行编译。在终端输入命令hb set,选择standard和unionpi_whale,在终端输入命令hb build -T tcp_demo。
- 对编译有疑问的读者可查看笔者另外一篇文章【FFH】OpenHarmony构建编译实战,此处不做赘述。
- 编译产物在out/board/product目录下。
5.烧录
- 全量烧录: 适合更新版本或者代码大变动打包镜像->RKDevTool烧录。
- HDC工具:适合代码更新时单独发送所需文件。
- 找到可执行文件tcp,并将其放置到电脑hdc.exe同级目录下。
- 连接设备:将开发板上电,并连接电脑。
- whale开发板烧录口为蓝色USB口上层口,使用USBtoUSB线烧录。
- 从hdc文件夹下进入终端,输入hdc list targets检查是否连接好,检测到设备后输-入hdc smode授予进程root权限,再输入hdc shell mount -o rw,remount /挂载分区,并且赋予可写权限。
- 输入hdc shell进入开发板终端,mkdir sample创建文件夹,exit退出终端。
- hdc file send ./tcp /sample/传输文件。(将当前目录下的hello文件传输到开发板的sample目录下)
- hdc shell再次进入开发板终端,cd sample进入文件夹,chmod 777 *给程序赋予可执行权限。
6.测试并执行
- 服务端程序测试:
将开发板连接上网络,通过hdc.exe工具执行命令ifconfig查看开发板IP地址:
- 通过netassist模拟客户端,选择TCP Client,填入开发板地址与端口号:
- 在sample目录下执行./tcp_demo命令,启动程序,终端打印提示信息:
- 点击netassist模拟客户端连接按钮,可以看到与开发板服务端连接成功,并接受到开发板发来的Hello, here is server!消息,终端也打印连接成功的提示信息。
- 点击发送按钮发送Welcome to NetAssist给开发板服务端,终端也打印相对应提示信息:
- 程序结束,测试成功。
客户端程序测试:
- 将开发板连接上网络。
- 使用ipconfig查看PC主机IP地址:
- 通过netassist模拟服务端,选择TCP Server,填入开发板地址与端口号,单击打开按钮开启监听:
- 在sample目录下执行./tcp_demo命令,启动程序,输入服务端IP与端口号:
- 可见终端打印连接成功的提示信息,主机收到了开发板客户端发来的Hello, here is client!消息:
- 点击发送按钮发送Welcome to NetAssist给开发板客户端,终端也打印相对应提示信息:
- 程序结束,测试成功。
7.番外
此程序意在说明一台设备既可以作为客户端也可以作为服务端,程序实现了本机先作为服务端与PC主机进行TCP通信,后两者交换身份,本机作为客户端。笔者测试过,程序无误可以正常运行,在此不做赘述。有兴趣的读者可以作尝试,
"C:\Users\LIGANG\Desktop\Mysample\tcp_demo\src\tcp_demo.cpp"
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define SERVER_PORT1 4567 // 服务器端口号
#define SERVER_PORT2 7654 // 服务器端口号
#define MAX_CLIENTS 5 // 最大客户端数量
#define TCP_BUFFER_SIZE 1024 // 缓冲区大小
int main()
{
// ------------------------------------本机作为服务端--------------------------------------------
int server_sock, client_sock; // 服务器套接字和客户端套接字
struct sockaddr_in server_addr, client_addr; // 服务器地址和客户端地址
socklen_t client_addr_len = sizeof(client_addr); // 客户端地址长度
// 创建一个服务端TCP套接字
server_sock = socket(AF_INET, SOCK_STREAM, 0);
if(server_sock == -1)
{
perror("Failed to create socket");
exit(EXIT_FAILURE);
}
// 设置服务端地址
server_addr.sin_family = AF_INET; // 使用IPv4协议
server_addr.sin_port = htons(SERVER_PORT1); // 端口号
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的IP地址
// 绑定套接字
if (bind(server_sock, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1)
{
perror("Failed to bind socket");
close(server_sock);
exit(EXIT_FAILURE);
}
// 开始监听客户端连接请求
if (listen(server_sock, MAX_CLIENTS) == -1)
{
perror("Failed to listen");
close(server_sock);
exit(EXIT_FAILURE);
}
std::cout << "Server is listening on port " << SERVER_PORT1 << std::endl;
while(true)
{
// 接受客户端连接请求
client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_sock == -1)
{
perror("Failed to accept connection");
continue;
}
// 打印出连接成功的客户端的IP地址和端口号
std::cout<<"Accepted connection from "<<inet_ntoa(client_addr.sin_addr)<<":"<<ntohs(client_addr.sin_port)<<std::endl;
// 发送数据给客户端
char request[] = "Hello, here is server!";
size_t bytes_write = write(client_sock, request, strlen(request));
if (bytes_write == -1)
{
perror("Failed to write data");
close(client_sock);
exit(EXIT_FAILURE);
}
// 接受客户端发送的数据
char buffer[TCP_BUFFER_SIZE];
size_t bytes_read = read(client_sock, buffer, TCP_BUFFER_SIZE);
if (bytes_read == -1)
{
perror("Failed to read data");
close(client_sock);
exit(EXIT_FAILURE);
}
else
{
std::cout<<"Received data from client: "<<buffer<<std::endl;
break;
}
}
// 关闭
close(server_sock);
// ------------------------------------本机作为客户端--------------------------------------------
// 配置服务器地址信息
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT2);
// 原来的客户端地址作为服务端地址
server_addr.sin_addr.s_addr = client_addr.sin_addr.s_addr;
// 创建客户端套接字
client_sock = socket(AF_INET, SOCK_STREAM, 0);
if(client_sock == -1)
{
perror("Failed to create socket");
exit(EXIT_FAILURE);
}
// 连接服务器
while(true)
{
// 连接服务器
if (connect(client_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("Failed to connect to server");
close(client_sock);
exit(EXIT_FAILURE);
}
// 打印出连接成功的服务器的IP地址和端口号
std::cout<<"Connected to server "<<inet_ntoa(server_addr.sin_addr)<<":"<<ntohs(server_addr.sin_port)<<std::endl;
// 向服务器发送数据
char request[] = "Hello, here is client!";
size_t bytes_write = write(client_sock, request, strlen(request));
if (bytes_write == -1)
{
perror("Failed to write data");
close(client_sock);
exit(EXIT_FAILURE);
}
// 从服务器接收数据
char buffer[TCP_BUFFER_SIZE];
size_t bytes_read = read(client_sock, buffer, TCP_BUFFER_SIZE);
if (bytes_read == -1)
{
perror("Failed to read data");
close(client_sock);
exit(EXIT_FAILURE);
}
else
{
std::cout<<"Received data from server: "<<buffer<<std::endl;
break;
}
}
// 关闭
close(client_sock);
return 0;
}