OpenHarmony TCP 通信編程實戰
前言
本人是一名大一學生,有幸被選拔進了深圳技術大學第一屆開源鴻蒙菁英班,并在暑期培訓進行線上分享,故將講解的內容也制作成帖子發上來作為學習筆記。在準備分享的過程中,我基于學長們的先前成果,結合開源鴻蒙源碼的最新版本進行了相應的調整和優化,幫助大家更好地理解和應用開源鴻蒙技術。本文旨在探討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()函數通??梢栽O置一些參數,比如要發送的數據長度、發送數據的起始位置等。
- 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("http://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": [
"http://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;
}