【Linux網絡篇】:Socket網絡套接字以及簡單的UDP網絡程序編寫

?感謝您閱讀本篇文章,文章內容是個人學習筆記的整理,如果哪里有誤的話還請您指正噢?
? 個人主頁:余輝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.理解源端口號和目的端口號

    傳輸協議層(TCPUDP)的數據段中有兩個端口號,分別叫做源端口號和目的端口號,就是在描述**”數據是誰發的,要發給誰“**。

    源端口號

    • 標識發送數據包的進程
    • 通常是動態分配的臨時端口,用于客戶端發起請求;
    • 作用:確保服務器返回的相應能正確回到發起請求的進程

    目的端口號

    • 標識接收數據包的進程
    • 通常是固定端口
    • 作用:告訴目標主機將數據包交給哪個進程處理

3.初步認識TCP協議和UDP協議

此處先對TCP(傳輸控制協議)以及UDP(用戶數據協議)有一個直觀的認識;后面再詳細講解細節問題。

TCP協議

  • 傳輸層協議

  • 有連接

    簡單理解就是打電話前需要先撥號,雙方”接通“后才能對話;通信前需要建立一條”專屬通道“,結束后要掛斷。

  • 可靠傳輸

    傳輸過程中數據不會丟失,如果丟失,TCP協議可以重新發送;數據順序不亂,TCP保證數據按序到達;確認機制,數據傳輸完后,需要等待對方確認收到數據才能結束,否則會一直重傳。

  • 面向字節流

UDP協議

  • 傳輸層協議

  • 無連接

    簡單理解就是直接發送短信或郵件,無需撥號或等待對方接聽,不關心對方是否收到。

  • 不可靠傳輸

    數據傳輸過程中可能丟包,數據可能被丟棄,但發送的并不知道(沒有重傳機制);順序混亂,數據可能亂序到達;無確認,發送完即結束,對方是否收到無法確認。

  • 面向數據報

4.網絡字節序

1.什么是字節序?

當一個多字節的數據(比如一個16位的短整型short或一個32位的整形int)存儲在內存中時,他的字節有兩種排列方式:

  • 大端序:高位字節存儲在內存的低地址,低位字節存儲在內存的高地址;
  • 小端序:低位字節存儲在內存的低地址,高位字節存儲在內存的高地址;

例如,一個16位的整數0x1234(十進制的4660):

  • 高位字節是0x12
  • 低位字節是0x34

在內存中(假設起始地址是0x1000):

  • 大端序存儲

    • 地址0x10000x12
    • 地址0x10010x34
  • 小端序存儲

    • 地址0x10000x34
    • 地址0x10010x12

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_instruct 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:發送標志,通常為0
    • dest_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:接收標志,通常為0
    • dest_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.0INADDR_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網絡程序編寫的講解,如果哪里有錯的話,可以在評論區指正,也歡迎大家一起討論學習,如果對你的學習有幫助的話,點點贊關注支持一下吧!!!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/84456.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/84456.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/84456.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Python爬蟲實戰:研究Newspaper框架相關技術

1. 引言 1.1 研究背景與意義 互聯網的快速發展使得新聞信息呈現爆炸式增長&#xff0c;如何高效地獲取和分析這些新聞數據成為研究熱點。新聞爬蟲作為一種自動獲取網頁內容的技術工具&#xff0c;能夠幫助用戶從海量的互聯網信息中提取有價值的新聞內容。本文基于 Python 的 …

【node.js】實戰項目

個人主頁&#xff1a;Guiat 歸屬專欄&#xff1a;node.js 文章目錄 1. 項目概覽與架構設計1.1 實戰項目&#xff1a;企業級電商管理系統1.2 技術棧選擇 2. 項目初始化與基礎架構2.1 項目結構設計2.2 基礎配置管理 3. 用戶服務實現3.1 用戶服務架構3.2 用戶模型設計3.3 用戶服務…

Mybatis框架的構建(IDEA)

選擇maven項目 修改設置 在設置中添加自定義代碼模板 開始寫代碼 動態SQL語句的示例&#xff1a; pom文件&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"…

經濟法-6-公司法律制度知識點

一、出資期限 1.有限責任公司&#xff1a;全體股東需在公司成立之日起5年內繳足認繳的注冊資本 2.股份有限公司&#xff1a;以發起方式設立的&#xff0c;發起人需在公司登記前實繳全部股款 3.認繳期加速到期 公司不能清償到期債務的&#xff0c;公司或者已到期債權的債權人…

jquery.table2excel方法導出

jquery提供了一個table2excel方法可以用來導出頁面到xls等 $("#grid_595607").table2excel({exclude: ".noExport", // 排除類名為 noExport 的元素filename: "導出數據.xls",exclude_img: true, // 不導出圖片exclude_links: true, // 不導…

echarts設置標線和最大值最小值

echarts設置標線和最大值最小值 基本ECharts圖表初始化配置 設置動態的y軸范圍&#xff08;min/max值&#xff09; 通過markPoint標記最大值和最小值點 使用markLine添加水平參考線 配置雙y軸圖表 自定義標記點和線的樣式&#xff08;顏色、符號等&#xff09; 響應式調整圖表大…

Java文件操作:從“Hello World”到“Hello File”

&#x1f50d; 開發者資源導航 &#x1f50d;&#x1f3f7;? 博客主頁&#xff1a; 個人主頁&#x1f4da; 專欄訂閱&#xff1a; JavaEE全棧專欄 文件 什么是文件&#xff1f; 廣義&#xff1a;操作系統進行資源管理的一種機制&#xff0c;很多的軟件/硬件資源&#xff0c;…

2025第三屆黃河流域網絡安全技能挑戰賽--Crypto--WriteUp

2025第三屆黃河流域網絡安全技能挑戰賽–Crypto–WriteUp Crypto sandwitch task from Crypto.Util.number import * import gmpy2 flag bflag{fake_flag} assert len(flag) 39 p getPrime(512) q getPrime(512) n p * q e 0x3 pad1 beasy_problem pad2 bHow_to_so…

三重天理論

第一重天&#xff1a;公理層&#xff08;形而上地基&#xff09; 這里構建的是人類理性的"操作系統"&#xff0c;公理作為不證自明的邏輯起點&#xff08;如矛盾律/同一律&#xff09;&#xff0c;恰似海德格爾所說的"存在之鏡"。黑格爾辯證法在此顯現為動…

2025年第八屆廣西大學生程序設計大賽(正式賽)題解(更新中)

知乎評價&#xff1a;如何評價2025年第八屆GXCPC廣西大學生程序設計大賽暨中國-東盟國際大學生程序設計大賽&#xff1f; 榜單&#xff1a;牛客比賽排名 題目鏈接&#xff1a;第八屆廣西大學生程序設計大賽暨2025邀請賽 TIP&#xff1a;提交處可查看別人過題代碼 難度簽到題普通…

WHAT - 兆比特每秒 vs 兆字節每秒

文章目錄 Mbps 解釋Mbps 和 MB/s&#xff08;兆字節每秒&#xff09;換算總結網絡場景1. 在路由器設置中的 Mbps2. 在游戲下載時的 Mbps / MB/s總結 Mbps 解釋 首先&#xff0c;Mbps 是一個常見的網絡帶寬單位&#xff0c;意思是&#xff1a; Megabits per second&#xff08;…

[C語言實戰]C語言內存管理實戰:實現自定義malloc與free(四)

[C語言實戰]C語言內存管理實戰&#xff1a;實現自定義malloc與free&#xff08;四&#xff09; 摘要&#xff1a;通過實現簡化版的內存管理器&#xff0c;深入理解動態內存分配的核心原理。本文包含內存塊設計、分配算法、空閑合并策略的完整實現&#xff0c;并附可運行的代碼…

YOLOv8源碼修改(5)- YOLO知識蒸餾(下)設置蒸餾超參數:以yolov8-pose為例

目錄 前言 1. 不同蒸餾算法資源占用 2. 不動態調整蒸餾損失 2.1 訓練定量化結果 2.1 訓練結果可視化結果 3. 動態調整蒸餾損失權重及實驗分析 3.1 余弦衰減和指數衰減 3.2 CWD蒸餾損失 3.3 MGD蒸餾損失 3.4 AT蒸餾損失 3.5 SKD和PKD蒸餾損失 4. 調權重心得總結 5…

歷年華東師范大學保研上機真題

2025華東師范大學保研上機真題 2024華東師范大學保研上機真題 2023華東師范大學保研上機真題 在線測評鏈接&#xff1a;https://pgcode.cn/school?classification1 簡單一位數代數式計算 題目描述 給一個小學生都會算的1位數與1位數運算的代數式&#xff0c;請你求出這個表…

Oracle 中 SHRINK 與 MOVE 操作的比較

Oracle 中 SHRINK 與 MOVE 操作的比較 在 Oracle 數據庫中&#xff0c;SHRINK 和 MOVE 都是用于重組表和索引以減少空間碎片的重要操作&#xff0c;但它們在實現方式和適用場景上有顯著區別。 SHRINK 操作 基本語法 ALTER TABLE table_name SHRINK SPACE [COMPACT] [CASCAD…

展銳 Android 15 鎖定某個App版本的實現

Android 15 系統鎖定Antutu版本的實現方法 在Android系統開發中,有時需要鎖定特定應用的版本以確保系統穩定性或測試一致性。本文將介紹如何通過修改Android源碼來鎖定Antutu跑分軟件的版本。 修改概述 這次修改主要涉及以下幾個方面: 禁用產品復制文件的檢查添加指定版本…

視頻剪輯SDK定制開發技術方案與報價書優雅草卓伊凡

視頻剪輯SDK定制開發技術方案與報價書-優雅草卓伊凡 一、項目概述 客戶需求&#xff1a;開發一套跨平臺&#xff08;Android/iOS/Uni-App&#xff09;視頻剪輯SDK&#xff0c;包含AI字幕提取、轉場特效、文字疊加、背景音樂、濾鏡、背景替換、動態貼紙等功能。 報價范圍&#…

BGP為什么要配置對等IP?

本文由deepseek生成&#xff0c;特此聲明 一、為什么要配置對等體IP&#xff1f; 1. 明確標識鄰居身份 路由協議需求&#xff1a;動態路由協議&#xff08;如BGP、OSPF、RIP&#xff09;需要路由器之間建立鄰居關系以交換路由信息。配置對等體IP是為了唯一標識鄰居路由器&…

Qt中配置文件讀寫

1. 保存分組數據到配置文件 #include <QSettings>void saveNetworkConfig() {QSettings settings("network.ini", QSettings::IniFormat);// 網絡配置分組settings.beginGroup("Network");// 源地址配置settings.beginGroup("Source");se…

Linux 的編輯器--vim

1.Linux編輯器-vim使? vi/vim的區別簡單點來說&#xff0c;它們都是多模式編輯器&#xff0c;不同的是vim是vi的升級版本&#xff0c;它不僅兼容vi的所有指令&#xff0c;?且還有?些新的特性在??。例如語法加亮&#xff0c;可視化操作不僅可以在終端運?&#xff0c;也可以…