?感謝您閱讀本篇文章,文章內容是個人學習筆記的整理,如果哪里有誤的話還請您指正噢?
? 個人主頁:余輝zmh–CSDN博客
? 文章所屬專欄:Linux篇–CSDN博客
文章目錄
- 網絡編程套接字
- 一.預備知識
- 1.理解源IP地址和目的IP地址
- 2.認識端口號
- 3.初步認識TCP協議和UDP協議
- 4.網絡字節序
- 5.sockaddr結構
- 二.簡單的UDP網絡程序
- 相關接口
- 代碼實現
- 應用場景
網絡編程套接字
一.預備知識
1.理解源IP地址和目的IP地址
在IP協議層的數據報中,有兩個IP地址,分別叫做源IP地址和目的IP地址。
源IP地址:發送數據包的設備的IP地址;告訴接收方數據包從哪里來。
目的IP地址:接收數據包的設備的IP地址;路由器根據目的IP地址決定數據包的轉發路徑。
2.認識端口號
先回答一個問題:在進行網絡通信的時候,是不是我們的兩臺機器在進行通信呢?
首先網絡協議棧中的下三層(傳輸層,網絡層,數據鏈路層),主要解決的是數據安全可靠的送到遠端機器;安全的發送不是目的,主要目的是收到數據后進行加工處理;而用戶需要使用應用層軟件,完成數據發送和接受,使用軟件首先要啟動軟件,軟件啟動后,在系統層面就是一個進程!
所以日常網絡通信的本質就是進程間的通信;通過網絡協議棧,借助網絡這個共享資源,實現兩臺不同的主機上的兩個不同的進程進行通信。
而一臺設備上可能同時運行多個網絡應用程序(比如瀏覽器,郵件客戶端,游戲服務器等),這時候傳輸層就需要明確知道當前發送的數據包具體要交給哪個程序處理,這里就需要借助端口號來實現。
-
1.端口號是傳輸層協議的內容:
端口號是一個2字節16位的整數;可以唯一的標識當前設備上的一個網絡應用程序;
而IP地址能夠表示唯一的一臺設備。
兩者結合使用:共同確定數據包的最終目的地—哪臺設備上的哪個進程應該接受或發送數據;這種技術就是套接字。
IP地址+端口號 = 套接字(Socket)
套接字是網絡通信中的端點,格式為:
IP地址:端口號
。 -
2.如何理解端口號和進程PID
在系統中,PID表示唯一的一個進程,而此處端口號也是表示唯一的一個進程,那為什么網絡通信時不直接用PID,而是要用端口號?
最容易理解的一點就是:PID屬于操作系統內部的進程管理,是系統模塊的;而端口號則是用來網絡通信中定位目標進程的,是網絡模塊的;兩者不同的用途,實現系統和網絡模塊之間的解耦,滿足模塊之間低耦合的要求。
此外,一個進程可以綁定多個端口號;但是一個端口號不能被多個進程綁定(因為不能滿足唯一性)。
-
3.理解源端口號和目的端口號
傳輸協議層(
TCP
和UDP
)的數據段中有兩個端口號,分別叫做源端口號和目的端口號,就是在描述**”數據是誰發的,要發給誰“**。源端口號:
- 標識發送數據包的進程;
- 通常是動態分配的臨時端口,用于客戶端發起請求;
- 作用:確保服務器返回的相應能正確回到發起請求的進程
目的端口號:
- 標識接收數據包的進程;
- 通常是固定端口;
- 作用:告訴目標主機將數據包交給哪個進程處理
3.初步認識TCP協議和UDP協議
此處先對TCP
(傳輸控制協議)以及UDP
(用戶數據協議)有一個直觀的認識;后面再詳細講解細節問題。
TCP協議:
傳輸層協議
有連接
簡單理解就是打電話前需要先撥號,雙方”接通“后才能對話;通信前需要建立一條”專屬通道“,結束后要掛斷。
可靠傳輸
傳輸過程中數據不會丟失,如果丟失,TCP協議可以重新發送;數據順序不亂,TCP保證數據按序到達;確認機制,數據傳輸完后,需要等待對方確認收到數據才能結束,否則會一直重傳。
面向字節流
UDP協議:
傳輸層協議
無連接
簡單理解就是直接發送短信或郵件,無需撥號或等待對方接聽,不關心對方是否收到。
不可靠傳輸
數據傳輸過程中可能丟包,數據可能被丟棄,但發送的并不知道(沒有重傳機制);順序混亂,數據可能亂序到達;無確認,發送完即結束,對方是否收到無法確認。
面向數據報
4.網絡字節序
1.什么是字節序?
當一個多字節的數據(比如一個16位的短整型short
或一個32位的整形int
)存儲在內存中時,他的字節有兩種排列方式:
- 大端序:高位字節存儲在內存的低地址,低位字節存儲在內存的高地址;
- 小端序:低位字節存儲在內存的低地址,高位字節存儲在內存的高地址;
例如,一個16位的整數0x1234
(十進制的4660):
- 高位字節是
0x12
; - 低位字節是
0x34
;
在內存中(假設起始地址是0x1000
):
-
大端序存儲:
- 地址
0x1000
:0x12
; - 地址
0x1001
:0x34
;
- 地址
-
小端序存儲:
- 地址
0x1000
:0x34
; - 地址
0x1001
:0x12
;
- 地址
2.什么是網絡字節序?
由于不同計算機的字節序可能不同,如果直接在網絡上傳輸多字節數據,接收方可能會錯誤的解釋這些數據。為了解決這個問題,TCP/IP
協議棧規定了一個統一的網絡字節序,這個標準就是大端序。
所有在網絡上傳輸的多字節數據(比如端口號,IP地址等)都必須轉換為網絡字節序(大端序)進行傳輸。接收方收到數據后,如果本機字節序與網絡字節序不同,就要將其轉換為本機字節序。
3.為什么需要轉換函數?
- 發送數據時:如果多字節數據,需要從主機字節序轉換為網絡字節序。
- 接收數據時:如果是多字節數據,需要從網絡字節序轉換為主機字節序。
4.網絡字節序轉換函數
C語言提供了一組標準函數來進行主機字節序和網絡字節序之間的轉換。這些函數名中的h
代表"host",n
代表"network",s
代表"short"(16位),l
代表"long"(32位)
頭文件:
#include <arpa/inet.h>
函數列表:
-
htons
:從主機字節序到網絡字節序(短整型16位)uint16_t htons(uint16_t hostshort);
-
htonl
:從主機字節序到網絡字節序(長整型32位)uint32_t htonl(uint32_t hostlong);
-
ntohs
:從網絡字節序到主機字節序(短整型16位)uint16_t ntohs(uint16_t netshort);
-
ntohl
:從網絡字節序到主機字節序(長整型32位)uint32_t ntohl(uint32_t netlong);
5.sockaddr結構
socket API
是一層抽象的網絡編程接口(具體的函數后面講解UDP和TCP時分別講解),適用于各種底層網絡協議,比如IPv4,IPv6。然而,各種網絡協議的地址格式并不相同。
具體有以下三種:
1.struct sockaddr
作用:
struct sockaddr
是通用的套接字地質結構體,用于在socker API
中傳遞地址參數。它本身并不包含具體的地址信息,而是作為其他地址結構體(比如struct sockaddr_in
,struct sockaddr_un
)的”父類“。
定義:
struct sockaddr {sa_family_t sa_family; // 地址族(如 AF_INET、AF_UNIX 等)char sa_data[14]; // 地址數據(具體內容由子類決定)
};
說明:
sa_family
指明了地址類型(比如IPv4,UNIX域等)。sa_data
是一個通用的字節數組,具體內容由實際的地址類型決定。- 在實際使用時,通常將具體的地質結構體(比如
sockaddr_in
)強制類型轉換為sockaddr*
傳遞給socket API。
2.struct sockaddr_in
作用:
struct sockaddr_in
是專門用于IPv4網絡地址的結構體,包含了IP地址和端口號等信息。常用于基于IPv4的網絡通信(比如UDP,TCP)。
定義:
#include <netinet/in.h>
struct sockaddr_in {sa_family_t sin_family; // 地址族,必須為 AF_INETin_port_t sin_port; // 端口號(網絡字節序)struct in_addr sin_addr; // IPv4 地址(網絡字節序)unsigned char sin_zero[8];// 填充字節,保證結構體大小與 sockaddr 一致
};
sin_family
:地址族,必須設置為AF_INET
。sin_port
:端口號,需用htons()
轉換為網絡字節序。sin_addr
:IPv4地址,需用inet_addr()
或inet_pton()
轉換為網絡字節序。sin_zero
:填充字段,無實際意義,只是為了結構體對齊。
3.struct sockaddr_un
作用:
struct sockaddr_un
是用于本地(UNIX域)套接字通信的結構體,常用于同一臺主機上的進程通信,而不經過網絡協議棧。
定義:
#include <sys/un.h>
struct sockaddr_un {sa_family_t sun_family; // 地址族,必須為 AF_UNIXchar sun_path[108]; // 文件系統路徑,表示本地套接字文件
};
sun_family
:地址族,必須設置為AF_UNIX
。sun_path
:本地套接字文件路徑,最大長度一般為108字節。
總結與區別
sockaddr
是通用”父類“,實際用時需強轉。sockaddr_in
用于IPv4網絡通信。sockaddr_un
用于本地(UNIX域)套接字通信。在實際編程中,API要求
struct sockaddr*
,傳遞struct sockaddr_in*
或struct sockaddr_un*
時需要強制類型轉換,這是網絡編程的常見用法。
二.簡單的UDP網絡程序
相關接口
1.socket
函數
int socket(int domain, int type, int protocol);
- 功能:創建套接字
- 參數:
domain
:協議族,比如AF_INET
(IPv4);type
:套接字類型,SOCK_DGRAM
(UDP),SOCK_STREAM
(TCP);protocol
:協議,通常為0;
- 返回值:成功返回套接字描述符
sockfd
(類似于文件描述符),后續所有的操作都依賴這個描述符;失敗返回-1。
2.bind
函數
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
功能:綁定地址和端口
-
參數:
sockfd
:套接字描述符addr
:地址結構體指針addrlen
:地址結構體長度
-
返回值:成功返回0,失敗返回-1
-
使用示例:
struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(8080); // 端口號 local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPif (bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {perror("bind error");return -1; }
3.sendto
函數
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
-
功能:發送數據
-
參數:
sockfd
:套接字描述符buf
:要發送的數據len
:發送數據的長度flags
:發送標志,通常為0dest_addr
:目標地址結構體addrlen
:地址結構體長度
-
返回值:成功返回發送的字節數,失敗返回-1。
-
注意事項:
- 如果是服務端使用該函數將數據發送給客戶端,該函數參數中的地址結構體填充的就是客戶端的相關信息;
- 反之,客戶端發送給服務端,填充的就是服務端的信息。
-
使用示例:
struct sockaddr_in client; client.sin_family = AF_INET; client.sin_port = htons(8080); client.sin_addr.s_addr = inet_addr("127.0.0.1");char buffer[] = "Hello"; sendto(sockfd, buffer, strlen(buffer), 0,(struct sockaddr*)&client, sizeof(client));
4.recvfrom
函數
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, sockaddr *src_addr, socklen_t *addrlen);
-
功能:接收數據
-
參數:
sockfd
:套接字描述符buf
:用來接收數據的緩沖區len
:緩沖區的長度flags
:接收標志,通常為0dest_addr
:源地址結構體,輸出型參數,用來獲取發送數據一方的地址結構體信息addrlen
:地址結構體長度指針,也是輸出型參數,用來獲取發送數據一方的地址結構體長度
-
返回值:成功返回接收的字節數,失敗返回-1。
-
注意事項:
- 如果是服務端調用該函數接收客戶端發送的數據,地址結構體中就是客戶端的信息,用來之后向客戶端發送數據;
- 如果是客戶端調用該函數接收服務端發送的,地址結構體中就是服務端的信息,用來之后向服務端發送數據。
-
使用示例:
char buffer[1024]; struct sockaddr_in client; socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer), 0,(struct sockaddr*)&client, &len); if (n > 0) {buffer[n] = 0;printf("收到數據:%s\n", buffer); }
5.close
函數
int close(int sockfd);
- 功能:關閉套接字
- 參數:
sockfd
:要關閉的套接字描述符
- 返回值:成功返回0;失敗返回-1。
代碼實現
基于上面的預備知識以及相關接口,實現一個自己的,可以相互發送接受數據的服務端與客戶端。
服務端:udpserver.hpp
#pragma once#include <iostream>
#include "log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <string.h>#define SIZE 1024using func_t = std::function<std::string(const std::string &)>;Log log;enum{SOCKET_ERR=1,INADDR_ERR,BIND_ERR,PORT_ERR,
};const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";class UDPServer{
public:UDPServer(const uint16_t port=defaultport,const std::string ip=defaultip):_sockfd(0),_port(port),_ip(ip),_isrunning(false){// 檢查端口號是否合法if(_port < 1024){log(Fatal, "Port number %d is too low, please use a port number > 1024", _port);exit(PORT_ERR);}} void Init(){// 1.創建udp socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){log(Fatal, "server socket create error, sockfd: %d", _sockfd);exit(SOCKET_ERR);}log(INFO, "server socket create success, sockfd: %d", _sockfd);// 2.連接udp socketstruct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port); // 端口號從主機字節序轉換為網絡字節序// 檢查 IP 地址是否有效if (_ip == "0.0.0.0") {local.sin_addr.s_addr = htonl(INADDR_ANY); // 監聽所有網絡接口} else {local.sin_addr.s_addr = inet_addr(_ip.c_str());if (local.sin_addr.s_addr == INADDR_NONE) {log(Fatal, "Invalid IP address: %s", _ip.c_str());exit(INADDR_ERR);}}// 將創建的socket與本地的IP地址和端口號綁定if(bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "server bind error, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}log(INFO, "server bind success, errno: %d, strerror: %s", errno, strerror(errno));}void Run1(func_t fun){_isrunning = true;char buffer[SIZE];while(_isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);// recvform的后兩個參數位輸出型參數ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);if(n < 0){log(Warning, "server recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));continue;}buffer[n] = 0;std::string info = buffer;// 模擬一次數據處理std::string echo_string = fun(info);std::cout << echo_string << std::endl;// 將處理后的數據發送到目標地址sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr *)&client, len);}}~UDPServer(){if(_sockfd > 0){close(_sockfd);}}private:int _sockfd; // 網絡文件描述符uint16_t _port; // 端口號std::string _ip; // ip地址bool _isrunning;
};
主程序:main.cc
#include "udpserver.hpp"
#include <iostream>
#include <memory>void Usage(std::string proc){std::cout << "\n\rUsage: " << proc << " port[1024+]\n"<< std::endl;
}std::string Handler(const std::string &str){std::string ret = "Server get a message# ";ret += str;return ret;
}int main(int argc, char *argv[]){if (argc != 2){Usage(argv[0]);exit(0);}// 使用命令行參數動態調整端口號uint16_t port = std::stoi(argv[1]);std::unique_ptr<UDPServer> svr(new UDPServer(port));svr->Init();svr->Run1(Handler);return 0;
}
客戶端:udpclient
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"#define SIZE 1024Log log;void Usage(std::string proc){std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[]){// ./udpclient serverip serverportif (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 填充服務器的網絡地址結構體struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(server);// 創建client socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0){log(Fatal, "client socket create error, errno: %d, strerror: %s", errno, strerror(errno));exit(1);}log(INFO, "client socket create success, sockfd: %d", sockfd);// client bind由系統完成 在首次發送數據時bindstd::string message; char buffer[SIZE];while(true){std::cout << "Please Enter@ ";getline(std::cin, message);// 1.發送數據到serversendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&server, len);//std::cout << " sendto aready " << std::endl;// 2.從server接收數據struct sockaddr_in temp;socklen_t len_temp = sizeof(temp);ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len_temp);if(n < 0){log(Warning, "client recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));continue;}buffer[n] = 0;std::cout << buffer << std::endl;}close(sockfd);return 0;
}
注意事項:
1.IP地址:
在服務端實現中,對IP地址初始化時,一般建議設置成0.0.0.0
(INADDR_ANY
)。
原因是:可以監聽所有網絡接口,可以接受來自任何網絡接口的數據,包括本地回環接口(127.0.0.1
)。
除此之外還可以變得更加靈活,服務器不需要知道具體的IP地址,可以適應多網卡環境。
因為這里使用的是云服務器,如果在初始化時,設置成指定的IP地址,會出現以下錯誤:
常見原因:
- 指定的IP地址不存在
- 指定的IP地址不是本機的IP
- 網絡接口未啟用
- 指定的IP地址格式錯誤
所以在平常的使用或開發時一般建議使用INADDR_ANY
。
2.端口號:
在服務端實現時,對端口號的初始化值一般建議大于1024,因為使用小于1024的端口號需要root權限;
除此之外,如果使用的是云服務器,還需要在控制臺的安全組中開放對應的端口。
如果出現以下錯誤:
常見原因:
- 使用特權端口(<1024)沒有root權限
- 文件權限不足
- 目錄權限不足
- 系統安全策略限制
上面就是關于IP地址和端口號的注意事項,實際使用時,一定要注意這幾點。
效果演示:
應用場景
1.執行客戶端發送的指令
主程序:
修改服務器處理信息時的回調函數為執行指令:
#include "udpserver.hpp"
#include <iostream>
#include <memory>
#include <vector>
#include <string>void Usage(std::string proc){std::cout << "\n\rUsage: " << proc << " port[1024+]\n"<< std::endl;
}// 簡單的發送信息
std::string Handler(const std::string &str){std::string ret = "Server get a message# ";ret += str;std::cout << ret << std::endl;return ret;
}// 應用場景:執行客戶端發送的指令
bool SafeCheck(const std::string &cmd){std::vector<std::string> key_word = {"rm","mv","cp","kill","sudo","unlink","uninstall","yum","top","while"};for(auto word : key_word){auto it = cmd.find(word);if(it!=std::string::npos){return false;}}return true;
}
std::string ExcuteCommand(const std::string &cmd){std::cout << "server get a command: " << cmd << std::endl;// 判斷輸入的指令的是否危險if (!SafeCheck(cmd)){return "Bad Command";}// 創建一個管道并執行輸入的指令FILE *fp = popen(cmd.c_str(), "r");if (fp == nullptr){return "Command execute failed!";}// 從管道中讀取內容,直到讀取到空std::string ret;char buffer[4096];while(true){char *ok = fgets(buffer, sizeof(buffer), fp);if (ok == nullptr){break;}ret += buffer;}pclose(fp);return ret;
}int main(int argc, char *argv[]){if (argc != 2){Usage(argv[0]);exit(0);}// 使用命令行參數動態調整端口號uint16_t port = std::stoi(argv[1]);std::unique_ptr<UDPServer> svr(new UDPServer(port));svr->Init();//svr->Run1(Handler);svr->Run1(ExcuteCommand);return 0;
}
效果演示:
2.Windows與Linux不同系統間的網絡傳輸
在vs2022啟動一個Windows的客戶端:
#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>#pragma warning(disable:4996)#pragma comment(lib, "ws2_32.lib")uint16_t serverport = 18080;
std::string serverip = "1.117.74.41";int main() {WSADATA wsd;WSAStartup(MAKEWORD(2, 2), &wsd);// 填充服務器的網絡地址結構體struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == SOCKET_ERROR) {std::cout << "socker error" << std::endl;exit(1);}std::string message;char buffer[1024];while (true) {std::cout << "Please Enter@ ";getline(std::cin, message);// 1.發送數據到serversendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr*)&server, sizeof(server));//std::cout << " sendto aready " << std::endl;// 2.從server接收數據struct sockaddr_in temp;int len = sizeof(temp);int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);if (n < 0) {std::cout << "revform error" << std::endl;exit(2);}buffer[n] = 0;std::cout << buffer << std::endl;}closesocket(sockfd);WSACleanup();return 0;
}
左邊是Windows客戶端,右邊是Linux服務端:
3.多人聊天
服務端代碼修改:
修改內容:增加一個哈希表用來存儲已經發送過信息的用戶,根據用戶的IP地址來判斷是否是新用戶,如果不存在哈希表中就是新用戶,添加到哈希表中;服務器處理完某個用戶發送的信息后,將該信息發送給哈希表中的所有用戶。
#pragma once#include <iostream>
#include "log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <string.h>
#include <unordered_map>#define SIZE 1024using func_t = std::function<std::string(const std::string &)>;Log log;enum{SOCKET_ERR=1,INADDR_ERR,BIND_ERR,PORT_ERR,
};const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";class UDPServer{
private:void CheckUser(struct sockaddr_in &client, const uint16_t clientport, const std::string &clientip){auto it = online_user.find(clientip);if(it == online_user.end()){// 用戶不存在,添加到哈希表中 online_user.insert({clientip, client});std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;}}void BroadCast(const std::string &info, const uint16_t clientport, const std::string &clientip){// 信息處理std::string message = "[";message += clientip;message += ":";message += std::to_string(clientport);message += "]# ";message += info;std::cout << "server get a message: " << message << std::endl;// 依次編譯哈希表 將信息發送給每一個用戶for(const auto &user : online_user){socklen_t len = sizeof(user.second);sendto(_sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)(&user.second), len);}}public:UDPServer(const uint16_t port=defaultport,const std::string ip=defaultip):_sockfd(0),_port(port),_ip(ip),_isrunning(false){// 檢查端口號是否合法if(_port < 1024){log(Fatal, "Port number %d is too low, please use a port number > 1024", _port);exit(PORT_ERR);}} void Init(){// 1.創建udp socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){log(Fatal, "server socket create error, sockfd: %d", _sockfd);exit(SOCKET_ERR);}log(INFO, "server socket create success, sockfd: %d", _sockfd);// 2.連接udp socketstruct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port); // 端口號從主機字節序轉換為網絡字節序// 檢查 IP 地址是否有效if (_ip == "0.0.0.0") {local.sin_addr.s_addr = htonl(INADDR_ANY); // 監聽所有網絡接口} else {local.sin_addr.s_addr = inet_addr(_ip.c_str());if (local.sin_addr.s_addr == INADDR_NONE) {log(Fatal, "Invalid IP address: %s", _ip.c_str());exit(INADDR_ERR);}}// 將創建的socket與本地的IP地址和端口號綁定if(bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "server bind error, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}log(INFO, "server bind success, errno: %d, strerror: %s", errno, strerror(errno));}void Run1(func_t fun){_isrunning = true;char buffer[SIZE];while(_isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);// recvform的后兩個參數位輸出型參數ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);if(n < 0){log(Warning, "server recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));continue;}buffer[n] = 0;std::string info = buffer;// 模擬一次數據處理std::string echo_string = fun(info);// 將處理后的數據發送到目標地址sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr *)&client, len);}}// 多用戶聊天測試void Run2(){_isrunning = true;char buffer[SIZE];while(_isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);// recvform的后兩個參數位輸出型參數ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);if(n < 0){log(Warning, "server recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));continue;}buffer[n] = 0;std::string info = buffer;uint16_t clientport = ntohs(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr);// 檢查當前用戶是否已經在哈希表中CheckUser(client, clientport, clientip);// 將當前信息發送給所有用戶BroadCast(info, clientport, clientip);}}~UDPServer(){if(_sockfd > 0){close(_sockfd);}}private:int _sockfd; // 網絡文件描述符uint16_t _port; // 端口號std::string _ip; // ip地址bool _isrunning;std::unordered_map<std::string, struct sockaddr_in> online_user;
};
客戶端代碼修改:
修改內容:平常使用微信,QQ等群聊時,即使我們不在群里發送消息我們也會收到其他用戶發送的消息;所以用戶在客戶端的發送消息和接收消息一定是分開的,所以需要將上面的單進程客戶端修改為多線程,分別處理消息的發送和接收。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
//#include "log.hpp"#define SIZE 1024//Log log;struct ThreadData{struct sockaddr_in server;int sockfd;std::string serverip;
};void Usage(std::string proc){std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}void *recv_message(void *args){ThreadData *td = static_cast<ThreadData *>(args);char buffer[SIZE];while(true){// 2.從server接收數據memset(buffer, 0, sizeof(buffer));struct sockaddr_in temp;socklen_t len_temp = sizeof(temp);ssize_t n = recvfrom(td->sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len_temp);if(n < 0){//log(Warning, "client recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));continue;}buffer[n] = 0;// 將收到的信息打印到標準錯誤流2中 然后再重定向到終端設備上 模擬同一界面的發消息和收消息std::cerr << buffer << std::endl;}
}void *send_message(void *args){ThreadData *td = static_cast<ThreadData *>(args);std::string message;socklen_t len = sizeof(td->server);std::string welcome = td->serverip;welcome += " coming ...";sendto(td->sockfd, welcome.c_str(), welcome.size(), 0, (const struct sockaddr *)&(td->server), len);while(true){std::cout << "Please Enter@ ";getline(std::cin, message);// 1.發送數據到serversendto(td->sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&(td->server), len);}
}int main(int argc, char *argv[]){// ./udpclient serverip serverportif (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 填充服務器的網絡地址結構體ThreadData td;bzero(&td.server, sizeof(td.server));td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport);td.server.sin_addr.s_addr = inet_addr(serverip.c_str());td.serverip = serverip;// 創建client sockettd.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(td.sockfd < 0){//log(Fatal, "client socket create error, errno: %d, strerror: %s", errno, strerror(errno));exit(1);}//log(INFO, "client socket create success, sockfd: %d", td.sockfd);// client bind由系統完成 在首次發送數據時bind// 多線程執行數據的發送和接收pthread_t recver, sender;pthread_create(&recver, nullptr, recv_message, &td);pthread_create(&sender, nullptr, send_message, &td);// 線程回收pthread_join(recver, nullptr);pthread_join(sender, nullptr);close(td.sockfd);return 0;
}
主程序修改:
服務器啟動后調用另一個運行函數。
效果演示:
用戶一:
用戶一的IP地址是127.0.0.1
,本地用戶進行測試;
左邊上側用一個終端表示聊天框,下側用另一個終端表示輸入框;右邊則是正在運行的服務器。
用戶二:
用戶二的IP地址是1.117.74.41
,另一個主機用戶進行測試;
上是聊天框,下是輸入框。
以上就是關于Socket
網絡套接字以及簡單UDP網絡程序編寫的講解,如果哪里有錯的話,可以在評論區指正,也歡迎大家一起討論學習,如果對你的學習有幫助的話,點點贊關注支持一下吧!!!