V1.0 2024年6月5日 發佈於博客園 目錄理論代碼伺服器端客戶端 理論 伺服器端先運行, 能夠接收來自任何地方的多個客戶端發起的指向特定埠(這裡是50002)的TCP請求, 並和客端建立穩定的TCP連接. 沒有連接請求時等待, 有連接後先來後到的原則, 依次服務, 能夠相互通信. 當客戶端結 ...
目錄V1.0 2024年6月5日 發佈於博客園
理論
伺服器端先運行, 能夠接收來自任何地方的多個客戶端發起的指向特定埠(這裡是50002)的TCP請求, 並和客端建立穩定的TCP連接. 沒有連接請求時等待, 有連接後先來後到的原則, 依次服務, 能夠相互通信.
當客戶端結束請求後, 自動接通第二個客戶端, 為其服務. (不足: 由於從終端讀取要發送的數據會阻塞, 故而上一個客戶端結束後要手動輸入任意字元解除阻塞後才能自動接通下一個, 在下一個版本中修改)
客戶端: 向伺服器發起TCP連接請求, 並和伺服器相互通信.
由於TCP有3次握手和4次揮手┏(^0^)┛, 且TCP連接的網路會被運營商的NAT保留數小時甚至數天, 故而不需要打洞.
但只能是內網客戶端與公網服務端相互通信!
代碼
伺服器端
/**
* @file name : tcp_server.c
* @brief : TCP伺服器IP, 響應客戶端 埠號 與客戶端建立鏈接
* @author : [email protected]
* @date : 2024年6月5日
* @version : 1.0
* @note : 編譯命令 cc tcp_server.c -o tcp_server.out -pthread
* // 運行伺服器可執行文件 ./xxx 要監聽的埠
* 運行:./tcp_server.out 50001
* 輸入 exit 退出伺服器端
* 待解決: 當客戶端結束後, 伺服器端需要發送一個任意信息(無效), 接通下一個客戶端
* CopyRight (c) 2023-2024 [email protected] All Right Reseverd
*/
#include <stdio.h>
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#define BUF_SIZE 1024 // 緩衝區大小(位元組)
// 客戶端網路信息結構體
typedef struct
{
int sock_fd; // 套接字文件描述符
struct sockaddr_in socket_addr; // 定義套接字所需的地址信息結構體
socklen_t addr_len; // 目標地址的長度
char receive_msgBuffer[BUF_SIZE]; // 發送給客戶端的保活包, 表示我是伺服器, C<---NET--S
} ClientArgs_t;
int client_live_flag = -1; //-1預設 0表示退出 1表示線上
volatile sig_atomic_t stop = 0; // 添加易變的信號量
// 信號處理程式,當接收到 SIGINT 信號時(通常是按下 Ctrl+C),它將 stop 變數設置為 1。
void handle_sigint(int sig)
{
stop = 1;
}
/**
* @name ReceivedFromClient
* @brief 接收線程函數, 用於處理C-->S的信息
* @param client_args 線程常式參數, 傳入保活包的網路信息
* @note
*/
void *ReceivedFromClient(void *client_args)
{
// 用於傳入的是void* 需要強轉才能正確指向
ClientArgs_t *ka_client_args = (ClientArgs_t *)client_args;
while (!stop) // 信號處理,以便更優雅地退出程式。
{
ssize_t bytes_read = read(ka_client_args->sock_fd, ka_client_args->receive_msgBuffer, sizeof(ka_client_args->receive_msgBuffer));
if (bytes_read > 0)
{
printf("recv from [%s], data is = %s\n", inet_ntoa(ka_client_args->socket_addr.sin_addr), ka_client_args->receive_msgBuffer);
bzero(ka_client_args->receive_msgBuffer, sizeof(ka_client_args->receive_msgBuffer));
}
else if (bytes_read == 0)
{
printf("客戶端斷開連接\n");
break;
}
else
{
perror("讀取客戶端發送的數據錯誤");
break;
}
}
close(ka_client_args->sock_fd); // 關閉與該客戶端的鏈接
free(ka_client_args); // 釋放空間
client_live_flag = 0; // 客戶端退出
pthread_exit(NULL); // 退出子線程
}
int main(int argc, char const *argv[])
{
// 檢查參數有效性
if (argc != 2)
{
fprintf(stderr, "請正確輸入埠號: %s <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
signal(SIGINT, handle_sigint); // 捕捉信號
/************第一步: 打開套接字, 得到套接字描述符************/
// 1.創建TCP套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket == -1)
{
fprintf(stderr, "tcp套接字打開錯誤, errno:%d, %s\n", errno, strerror(errno));
exit(EXIT_FAILURE);
}
/************END*************/
/************第二步: 將套接字描述符與埠綁定************/
// 伺服器端的IP信息結構體
struct sockaddr_in server_addr;
// 配置伺服器地址信息 接受來自任何地方的數據 包有效 但只解析輸入埠範圍的包
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // 協議族,是固定的
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 目標地址 INADDR_ANY 這個巨集是一個整數,所以需要使用htonl轉換為網路位元組序
server_addr.sin_port = htons(atoi(argv[1])); // 目標埠,必須轉換為網路位元組序
// 綁定socket到指定埠
if (bind(tcp_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
fprintf(stderr, "將伺服器套接字文件描述符綁定IP失敗, errno:%d, %s\n", errno, strerror(errno));
close(tcp_socket);
exit(EXIT_FAILURE);
}
/************END*************/
/************第三步: 設置監聽信息************/
// 3.設置監聽 隊列最大容量是5
if (listen(tcp_socket, 5) < 0)
{
fprintf(stderr, "設置監聽失敗, errno:%d, %s\n", errno, strerror(errno));
close(tcp_socket);
exit(EXIT_FAILURE);
}
printf("伺服器已經運行, 開始監聽中...\n");
/************END*************/
while (!stop)
{
/************第四步: 等待連接************/
// 4.等待接受客戶端的連接請求, 阻塞等待有一個C請求連接
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
printf("從隊列出取出一個請求或等待新的客戶端連接\n");
int connect_fd = accept(tcp_socket, (struct sockaddr *)&client, &client_len); // 會阻塞
client_live_flag = 1;
printf("已經從隊列出取出一個請求, 連接成功\n");
// 此時得到該客戶端的新套接字, 使用子線程用於接收客戶端信息, 主線程發送信息給客戶端
if (connect_fd < 0)
{
if (errno == EINTR && stop)
{
break;
}
fprintf(stderr, "接受連接失敗, 隊列異常, errno:%d, %s\n", errno, strerror(errno));
continue;
}
/************END*************/
/*********************創建接收線程********************/
// 子線程專屬 客戶端信息結構體
ClientArgs_t *client_args = (ClientArgs_t *)malloc(sizeof(ClientArgs_t));
if (!client_args)
{
fprintf(stderr, "線程專屬 客戶端信息結構體 記憶體分配失敗\n");
close(connect_fd);
continue;
}
// 配置客戶端信息結構體, 將信息傳遞到子線程
client_args->addr_len = client_len;
client_args->sock_fd = connect_fd;
client_args->socket_addr = client;
memset(client_args->receive_msgBuffer, 0, BUF_SIZE);
pthread_t ReceivedFromClient_thread; // 用於接收客戶端傳回的信息 新線程的TID
// 創建接收線程 並將客戶端IP信息結構體信息傳入線程
if (pthread_create(&ReceivedFromClient_thread, NULL, ReceivedFromClient, (void *)client_args) != 0)
{
fprintf(stderr, "創建接收線程錯誤, errno:%d, %s\n", errno, strerror(errno));
close(connect_fd); // 關閉對客戶端套接字
free(client_args);
continue; // 進入下一個請求
}
pthread_detach(ReceivedFromClient_thread); // 線程分離 主要目的是使得線程在終止時能夠自動釋放其占用的資源,而不需要其他線程顯式地調用 pthread_join 來清理它。
/************END*************/
/************第五步: 主線程發送數據給客戶端************/
char buffer[BUF_SIZE]; // 存放要發的數據緩衝區
while (!stop)
{
if (client_live_flag == 0)
{
break;
}
// 清理緩衝區
memset(buffer, 0x0, sizeof(buffer));
// 接收用戶輸入的字元串數據
printf("請輸入要發送的字元串(輸入exit退出伺服器程式):");
fgets(buffer, sizeof(buffer), stdin);
// 將用戶輸入的數據發送給伺服器
if (send(connect_fd, buffer, strlen(buffer), 0) < 0)
{
perror("發送錯誤:");
break;
}
// 輸入了"exit",退出迴圈
if (strncmp(buffer, "exit", 4) == 0)
{
close(connect_fd);
printf("你輸入了exit, 伺服器端程式結束\n");
break;
}
}
}
close(tcp_socket);
printf("伺服器程式結束\n");
return 0;
}
客戶端
/**
* @file name : tcp_client.c
* @brief : 從終端輸入伺服器IP 埠號 與伺服器建立TCP連接 並相互通信
* @author : [email protected]
* @date : 2024年6月5日
* @version : 1.0
* @note : 編譯命令 cc tcp_client.c -o tcp_client.out -pthread
* 運行:./tcp_client.out 1xx.7x.1x.2xx 50001
* 輸入 exit 退出客戶端
* CopyRight (c) 2023-2024 [email protected] All Right Reseverd
*/
#include <stdio.h>
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#define BUF_SIZE 1024 // 緩衝區大小(位元組)
/**
* @name receive_from_server
* @brief 接收線程函數, 用於處理C-->S的信息
* @param arg 線程常式參數, 傳入伺服器的網路信息
* @note
*/
void *receive_from_server(void *arg)
{
int tcp_socket_fd = *(int *)arg;
char buf[BUF_SIZE];
while (1)
{
memset(buf, 0, sizeof(buf));
ssize_t bytes_received = recv(tcp_socket_fd, buf, sizeof(buf) - 1, 0);
if (bytes_received > 0)
{
printf("從伺服器接收到數據: %s\n", buf);
}
else if (bytes_received == 0)
{
printf("伺服器斷開連接\n");
break;
}
else
{
perror("接收錯誤");
break;
}
}
return NULL;
}
// 運行客戶端可執行文件 ./xxx 目標伺服器地址 伺服器埠
int main(int argc, char const *argv[])
{
// 檢查參數有效性
if (argc != 3)
{
fprintf(stderr, "從終端輸入的參數無效, errno:%d,%s\n", errno, strerror(errno));
exit(EXIT_FAILURE);
}
/************第一步: 打開套接字, 得到套接字描述符************/
int tcp_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > tcp_socket_fd)
{
fprintf(stderr, "tcp socket error,errno:%d,%s\n", errno, strerror(errno));
exit(EXIT_FAILURE);
}
/************END*************/
/************第二步: 調用connect連接遠端伺服器************/
struct sockaddr_in server_addr = {0}; // 伺服器IP信息結構體
// 配置伺服器信息結構體
server_addr.sin_family = AF_INET; // 協議族,是固定的
server_addr.sin_port = htons(atoi(argv[2])); // 伺服器埠,必須轉換為網路位元組序
server_addr.sin_addr.s_addr = inet_addr(argv[1]); // 伺服器地址 "192.168.64.xxx"
int ret = connect(tcp_socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret)
{
perror("連接錯誤:");
close(tcp_socket_fd);
exit(EXIT_FAILURE);
}
printf("伺服器連接成功...\n\n");
/************END*************/
/************第三步: 向伺服器發送數據************/
// 創建接收線程
pthread_t recv_thread;
if (pthread_create(&recv_thread, NULL, receive_from_server, &tcp_socket_fd) != 0)
{
perror("線程創建失敗");
close(tcp_socket_fd);
exit(EXIT_FAILURE);
}
pthread_detach(recv_thread); // 線程分離 主要目的是使得線程在終止時能夠自動釋放其占用的資源,而不需要其他線程顯式地調用 pthread_join 來清理它。
/* 向伺服器發送數據 */
char buf[BUF_SIZE]; // 數據收發緩衝區
for (;;)
{
// 清理緩衝區
memset(buf, 0, sizeof(buf));
// 接收用戶輸入的字元串數據
printf("請輸入要發送的字元串: ");
if (fgets(buf, sizeof(buf), stdin) == NULL)
{
perror("fgets error");
break;
}
// 將用戶輸入的數據發送給伺服器
ret = send(tcp_socket_fd, buf, strlen(buf), 0);
if (0 > ret)
{
perror("發送錯誤:");
break;
}
// 輸入了"exit",退出迴圈
if (0 == strncmp(buf, "exit", 4))
break;
}
/************END*************/
close(tcp_socket_fd);
printf("客戶端程式結束\n");
exit(EXIT_SUCCESS);
return 0;
}
本文來自博客園,作者:舟清颺,轉載請註明原文鏈接:https://www.cnblogs.com/zqingyang/p/18234204