day34 UDP套接字編程 可靠文件傳輸與實時雙向聊天系統
UDP文件傳輸
實現客戶端向服務器傳輸文件(如圖片)的功能,確保傳輸后文件內容完全一致且可正常打開。傳輸過程采用簡單的確認機制防止數據包丟失,傳輸完成后雙方程序自動退出。核心知識點包括:UDP套接字創建、文件讀寫、數據包分塊傳輸、傳輸結束標志處理。
客戶端代碼 (cli.c)
#include <arpa/inet.h> // 提供IP地址轉換函數
#include <fcntl.h> // 提供文件控制操作
#include <netinet/in.h> // 定義IPv4地址結構
#include <netinet/ip.h> // 定義IP協議相關結構
#include <stdio.h> // 標準輸入輸出函數
#include <stdlib.h> // 標準庫函數
#include <string.h> // 字符串操作函數
#include <sys/socket.h> // 套接字API
#include <sys/types.h> // 數據類型定義
#include <time.h> // 時間相關函數
#include <unistd.h> // POSIX系統調用typedef struct sockaddr *(SA); // 定義sockaddr指針別名,簡化類型轉換int main(int argc, char **argv)
{// 創建UDP套接字(AF_INET: IPv4, SOCK_DGRAM: 數據報套接字)int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (-1 == sockfd){perror("socket"); // 套接字創建失敗時打印錯誤return 1;}// 配置服務器地址結構struct sockaddr_in ser;ser.sin_family = AF_INET; // 地址族:IPv4ser.sin_port = htons(50000); // 端口號轉網絡字節序(50000)ser.sin_addr.s_addr = inet_addr("192.168.1.48"); // 服務器IP地址// 打開本地文件(只讀模式)int fd = open("/home/linux/1.png", O_RDONLY);if (-1 == fd){perror("open error\n");return 1;}int num = 0; // 累計已發送字節數char buf[1024] = {0}; // 數據緩沖區(1024字節塊)while (1){bzero(buf, sizeof(buf)); // 清空緩沖區int ret = read(fd, buf, sizeof(buf)); // 從文件讀取數據到緩沖區num += ret; // 累計傳輸字節數printf("num:%d\n", num); // 實時打印已發送字節數// 讀取結束或出錯時退出循環if (ret <= 0){break;}// 向服務器發送數據包sendto(sockfd, buf, ret, 0, (SA)&ser, sizeof(ser));// 接收服務器確認包(小阻塞:確保服務器處理完當前包)bzero(buf, sizeof(buf));recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);}// 發送傳輸結束標志strcpy(buf, "^_^"); // 結束標志字符串sendto(sockfd, buf, strlen(buf), 0, (SA)&ser, sizeof(ser));// 關閉資源close(sockfd);close(fd);return 0;
}
服務器代碼 (ser.c)
#include <arpa/inet.h> // 提供IP地址轉換函數
#include <netinet/in.h> // 定義IPv4地址結構
#include <netinet/ip.h> // 定義IP協議相關結構
#include <stdio.h> // 標準輸入輸出函數
#include <stdlib.h> // 標準庫函數
#include <string.h> // 字符串操作函數
#include <sys/socket.h> // 套接字API
#include <sys/types.h> // 數據類型定義
#include <unistd.h> // POSIX系統調用
#include <time.h> // 時間相關函數
#include <fcntl.h> // 提供文件控制操作typedef struct sockaddr * (SA); // 定義sockaddr指針別名int main(int argc, char **argv)
{// 創建UDP套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (-1 == sockfd){perror("socket");return 1;}// 配置服務器地址結構(用于綁定)struct sockaddr_in ser, cli;ser.sin_family = AF_INET; // 地址族:IPv4ser.sin_port = htons(50000); // 端口號轉網絡字節序ser.sin_addr.s_addr = inet_addr("192.168.1.48"); // 本機IP地址// 綁定套接字到指定地址和端口int ret = bind(sockfd, (SA) &ser, sizeof(ser));if (-1 == ret){perror("bind");return 1;}time_t tm; // 時間變量(未實際使用,保留原代碼)socklen_t len = sizeof(cli); // 客戶端地址結構長度// 創建新文件(寫入/創建/截斷模式,權限0666)int fd = open("2.png", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (-1 == fd){perror("open error\n");return 1;}int num = 0; // 累計接收字節數while (1){char buf[1024] = {0}; // 數據緩沖區// 接收客戶端數據包int ret = recvfrom(sockfd, buf, sizeof(buf), 0, (SA)&cli, &len);num += ret; // 累計接收字節數printf("num:%d\n", num); // 實時打印已接收字節數// 檢測到結束標志時退出循環if (0 == strcmp(buf, "^_^")){break;}// 將數據寫入文件write(fd, buf, ret);// 發送確認包(解除客戶端阻塞)bzero(buf, sizeof(buf));strcpy(buf, "go on"); // 確認消息sendto(sockfd, buf, strlen(buf), 0, (SA)&cli, len);}// 關閉資源close(sockfd);close(fd);return 0;
}
理想運行結果
- 文件傳輸過程:
- 客戶端輸出:
num:1024
→num:2048
→ … →num:[文件總大小]
(字節數遞增) - 服務器輸出:
num:1024
→num:2048
→ … →num:[文件總大小]
(與客戶端一致)
- 客戶端輸出:
- 傳輸驗證:
- 傳輸完成后,
2.png
與原始1.png
大小完全相同(ls -l
對比) - 圖片文件可正常打開(如
eog 2.png
無損壞)
- 傳輸完成后,
- 退出機制:
- 客戶端發送
^_^
后自動退出 - 服務器收到
^_^
后自動退出
- 客戶端發送
- 關鍵特性:
- 每個數據包傳輸后通過
recvfrom
/sendto
實現簡單確認,避免發送過快導致丟包 - 結束標志
^_^
確保雙方同步退出
- 每個數據包傳輸后通過
UDP聊天
實現雙向實時聊天功能,支持連續消息收發。輸入 #quit
時,客戶端和服務器同時退出。核心知識點包括:多進程并發處理(fork
)、消息循環控制、退出信號同步。
服務器代碼 (ser.c)
#include <arpa/inet.h> // IP地址轉換
#include <fcntl.h> // 文件控制
#include <netinet/in.h> // IPv4地址結構
#include <netinet/ip.h> // IP協議結構
#include <signal.h> // 信號處理(用于進程終止)
#include <stdio.h> // 標準I/O
#include <stdlib.h> // 標準庫
#include <string.h> // 字符串操作
#include <sys/socket.h> // 套接字API
#include <sys/types.h> // 數據類型
#include <time.h> // 時間函數
#include <unistd.h> // POSIX系統調用typedef struct sockaddr *(SA); // sockaddr指針別名int main(int argc, char **argv)
{// 創建UDP套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (-1 == sockfd){perror("socket");return 1;}// 配置服務器地址結構struct sockaddr_in ser, cli;ser.sin_family = AF_INET;ser.sin_port = htons(50000); // 監聽端口50000ser.sin_addr.s_addr = inet_addr("192.168.1.48"); // 本機IP// 綁定套接字int ret = bind(sockfd, (SA)&ser, sizeof(ser));if (-1 == ret){perror("bind");return 1;}socklen_t len = sizeof(cli); // 客戶端地址長度char buf[512] = {0}; // 消息緩沖區// 等待客戶端初始連接("start"消息)recvfrom(sockfd, buf, sizeof(buf), 0, (SA)&cli, &len);// 創建子進程處理并發pid_t pid = fork();if (pid > 0) // 父進程:接收客戶端消息{while (1){bzero(buf, sizeof(buf)); // 清空緩沖區// 接收客戶端消息recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);// 檢測退出命令if (0 == strcmp(buf, "#quit\n")){kill(pid, 9); // 終止子進程exit(1); // 父進程退出}printf("cli:%s", buf); // 顯示客戶端消息}}else if (0 == pid) // 子進程:發送服務器消息{while (1){printf("to cli:"); // 提示輸入char buf[512] = {0};fgets(buf, sizeof(buf), stdin); // 讀取用戶輸入// 發送消息到客戶端sendto(sockfd, buf, strlen(buf), 0, (SA)&cli, len);// 檢測退出命令if (0 == strcmp(buf, "#quit\n")){kill(getppid(), 9); // 終止父進程exit(1); // 子進程退出}}}else // fork失敗{perror("fork");return 1;}close(sockfd); // 關閉套接字(實際不會執行到此處)return 0;
}
客戶端代碼 (cli.c)
#include <arpa/inet.h> // IP地址轉換
#include <netinet/in.h> // IPv4地址結構
#include <netinet/ip.h> // IP協議結構
#include <stdio.h> // 標準I/O
#include <stdlib.h> // 標準庫
#include <string.h> // 字符串操作
#include <sys/socket.h> // 套接字API
#include <sys/types.h> // 數據類型
#include <unistd.h> // POSIX系統調用
#include <time.h> // 時間函數
#include <fcntl.h> // 文件控制
#include <signal.h> // 信號處理typedef struct sockaddr * (SA); // sockaddr指針別名int main(int argc, char **argv)
{// 創建UDP套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (-1 == sockfd){perror("socket");return 1;}// 配置服務器地址結構struct sockaddr_in ser;ser.sin_family = AF_INET;ser.sin_port = htons(50000); // 目標端口50000ser.sin_addr.s_addr = inet_addr("192.168.1.48"); // 服務器IPsocklen_t len = sizeof(ser); // 地址長度char buf[512] = {0};strcpy(buf, "start"); // 初始連接消息sendto(sockfd, buf, strlen(buf), 0, (SA)&ser, len); // 通知服務器// 創建子進程處理并發pid_t pid = fork();if (pid > 0) // 父進程:接收服務器消息{while (1){bzero(buf, sizeof(buf));// 接收服務器消息recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);// 檢測退出命令if (0 == strcmp(buf, "#quit\n")){kill(pid, 9); // 終止子進程exit(1); // 父進程退出}printf("ser:%s", buf); // 顯示服務器消息}}else if (0 == pid) // 子進程:發送客戶端消息{while (1){printf("to ser:"); // 提示輸入char buf[512] = {0};fgets(buf, sizeof(buf), stdin); // 讀取用戶輸入// 發送消息到服務器sendto(sockfd, buf, strlen(buf), 0, (SA)&ser, len);// 檢測退出命令if (0 == strcmp(buf, "#quit\n")){kill(getppid(), 9); // 終止父進程exit(1); // 子進程退出}}}else // fork失敗{perror("fork");return 1;}close(sockfd); // 關閉套接字(實際不會執行到此處)return 0;
}
理想運行結果
-
初始化連接:
- 客戶端發送
start
消息,服務器開始監聽 - 雙方進入消息循環
- 客戶端發送
-
聊天過程:
- 服務器終端:
to cli:Hello // 服務器輸入 cli:How are you? // 顯示客戶端消息 to cli:#quit // 輸入退出命令
- 客戶端終端:
ser:Hello // 顯示服務器消息 to ser:How are you? // 客戶端輸入 ser:#quit // 收到退出命令
- 服務器終端:
-
退出機制:
- 任意一方輸入
#quit
后:- 發送方立即終止對方進程(
kill
) - 本地進程退出(
exit(1)
)
- 發送方立即終止對方進程(
- 雙方終端同時關閉,無殘留進程
- 任意一方輸入
-
關鍵特性:
- 通過
fork
實現雙工通信:父進程處理接收,子進程處理發送 - 退出命令
#quit
觸發雙向終止(避免單方退出導致僵局) - 消息實時顯示(
printf
前無緩沖,即時刷新)
- 通過