??? 作者:@阿亮joy.
??專欄:《學(xué)會Linux》
?? 座右銘:每個優(yōu)秀的人都有一段沉默的時光,那段時光是付出了很多努力卻得不到結(jié)果的日子,我們把它叫做扎根
??預(yù)備知識??
源IP地址和目的IP地址
源 IP 地址指發(fā)送方的 IP 地址,而目的 IP 地址是指接收方的 IP 地址,源 IP 地址和目的 IP 地址是包含在數(shù)據(jù)包的IP 頭部(IP header)中的,IP 頭部是每個 IP 數(shù)據(jù)包都必須包含的一部分。這兩個地址在網(wǎng)絡(luò)傳輸過程中是不變的,因為它們是數(shù)據(jù)包的一部分,并且用于確定數(shù)據(jù)包的源和目的地。
在網(wǎng)絡(luò)傳輸過程中,每個節(jié)點都會讀取 IP 頭部的內(nèi)容,并基于其中的源 IP 地址和目的 IP 地址等信息來決定下一步的路由和轉(zhuǎn)發(fā)操作。有了源 IP 地址和目的 IP 地址,還不能完成網(wǎng)絡(luò)傳輸,還需要 MAC 地址。IP 地址用于標(biāo)識主機在網(wǎng)絡(luò)中的位置,MAC 地址用于標(biāo)識主機的網(wǎng)絡(luò)接口。在不同網(wǎng)絡(luò)之間的通信,通常需要使用 IP 地址進行路由和轉(zhuǎn)發(fā),而在同一局域網(wǎng)內(nèi)的通信,則需要使用 MAC地址進行直接傳輸。
端口號
端口號是傳輸層協(xié)議的內(nèi)容,它是用于標(biāo)識網(wǎng)絡(luò)應(yīng)用程序的通信端口的一個 16 位的數(shù)字,其取值范圍是 0 到 65535。其中 0 到 1023 的端口號被保留用于一些特定的服務(wù)和應(yīng)用程序,稱為“系統(tǒng)端口”或“熟知端口”,例如 HTTP 服務(wù)使用的端口號為 80,SMTP 服務(wù)使用的端口號為 25。每個端口號都與一個特定的應(yīng)用程序或服務(wù)相關(guān)聯(lián),用于區(qū)分同一主機上的不同應(yīng)用程序或在網(wǎng)絡(luò)上的不同主機上的不同應(yīng)用程序。
在一個網(wǎng)絡(luò)數(shù)據(jù)包中,源和目標(biāo)主機的 IP 地址用于標(biāo)識主機在網(wǎng)絡(luò)中的位置,而源和目標(biāo)端口號則用于標(biāo)識主機上的應(yīng)用程序(進程)。網(wǎng)絡(luò)應(yīng)用程序使用端口號來與其他應(yīng)用程序進行通信。在網(wǎng)絡(luò)傳輸中,源主機上的應(yīng)用程序?qū)?shù)據(jù)發(fā)送到目標(biāo)主機上的特定端口號,目標(biāo)主機會將數(shù)據(jù)包路由到相應(yīng)的應(yīng)用程序上進行處理。因此,端口號的作用是為應(yīng)用程序提供一種可靠的通信機制,使不同的應(yīng)用程序可以在同一主機上共存,或者在不同主機上進行通信。
注:IP地址 + 端口號能夠標(biāo)識網(wǎng)絡(luò)上的某一臺主機的某一個進程,一個端口號只能被一個進程占用,而一個進程可以綁定多個端口號。
PID和端口號的區(qū)別
PID 可以標(biāo)識唯一的進程,而端口號也能標(biāo)識唯一的一個進程。那為什么網(wǎng)絡(luò)通信不采用 PID 來表示唯一的進程呢?雖然 PID 在某些情況下可以用來標(biāo)識特定的應(yīng)用程序(進程),但在網(wǎng)絡(luò)中,PID 不是一種可靠的方式來標(biāo)識應(yīng)用程序,原因如下:
- PID 只在單個計算機上是唯一的:每個計算機上的進程都有自己的 PID,因此,在不同的計算機上運行的同一應(yīng)用程序具有不同的 PID。這意味著在網(wǎng)絡(luò)上使用 PID 來標(biāo)識應(yīng)用程序時,它只能標(biāo)識單個計算機上的應(yīng)用程序,而不能唯一地標(biāo)識整個網(wǎng)絡(luò)中的應(yīng)用程序。
- PID 是動態(tài)的:當(dāng)一個應(yīng)用程序在計算機上啟動時,它被分配一個 PID。但是,當(dāng)該應(yīng)用程序終止后,該 PID 將被釋放并可以被操作系統(tǒng)重新分配給其他進程。因此,使用 PID 來標(biāo)識應(yīng)用程序可能會導(dǎo)致標(biāo)識符沖突或標(biāo)識符混淆,因為一個新的應(yīng)用程序可能會被分配以前已經(jīng)被釋放的 PID。
相比之下,端口號是一種更可靠的方式來標(biāo)識網(wǎng)絡(luò)中的應(yīng)用程序,因為它在整個網(wǎng)絡(luò)中是唯一的,并且不會被操作系統(tǒng)重新分配給其他應(yīng)用程序。此外,端口號可以方便地被網(wǎng)絡(luò)管理人員和安全人員用于限制或控制網(wǎng)絡(luò)通信,從而增強網(wǎng)絡(luò)安全性。
套接字
套接字(socket)是一種用于在計算機網(wǎng)絡(luò)中進行通信的軟件設(shè)備,它提供了一種抽象層,使得應(yīng)用程序可以使用統(tǒng)一的接口來進行網(wǎng)絡(luò)通信,而無需了解底層網(wǎng)絡(luò)協(xié)議的復(fù)雜性。
套接字通常由一個 IP 地址和一個端口號組成,它們一起標(biāo)識了網(wǎng)絡(luò)中的一個特定的節(jié)點。一個套接字可以用來建立連接、發(fā)送和接收數(shù)據(jù),同時也可以被關(guān)閉和釋放。
在應(yīng)用程序中,套接字通常由操作系統(tǒng)提供的套接字庫進行管理。套接字庫提供了一組函數(shù),使得應(yīng)用程序可以方便地創(chuàng)建、綁定、監(jiān)聽和連接套接字,并進行數(shù)據(jù)的發(fā)送和接收。套接字庫也提供了一些高級函數(shù),如 select 和 poll,用于進行異步通信和多路復(fù)用。
套接字可以用于各種類型的網(wǎng)絡(luò)通信,如 TCP、UDP 和RAW 等協(xié)議。TCP 套接字提供了面向連接的、可靠的數(shù)據(jù)傳輸服務(wù),適用于需要可靠傳輸?shù)膽?yīng)用程序,如 Web 瀏覽器、郵件客戶端等;UDP 套接字則提供了無連接、不可靠的數(shù)據(jù)傳輸服務(wù),適用于實時性要求高、數(shù)據(jù)傳輸量較小的應(yīng)用程序,如在線游戲、語音聊天等;RAW 套接字則可以讓應(yīng)用程序直接訪問網(wǎng)絡(luò)協(xié)議棧,適用于需要自定義協(xié)議和進行網(wǎng)絡(luò)調(diào)試的應(yīng)用程序。
綜上,網(wǎng)絡(luò)編程也被稱為套接字編程。
認(rèn)識UDP協(xié)議
UDP 協(xié)議(用戶數(shù)據(jù)報協(xié)議)是一種無連接的、面向數(shù)據(jù)報、不可靠的協(xié)議,它不提供連接建立和數(shù)據(jù)校驗等功能,而是將數(shù)據(jù)直接打包成數(shù)據(jù)報發(fā)送,不保證數(shù)據(jù)的可靠性。UDP 適用于實時性要求較高的應(yīng)用,如在線游戲、語音聊天等,因為它具有低延遲、高吞吐量的優(yōu)勢。UDP 的優(yōu)點在于它的簡單、高效,但是由于它不保證數(shù)據(jù)的可靠性,因此需要應(yīng)用程序自己處理數(shù)據(jù)的錯誤和丟失等問題。
認(rèn)識TCP協(xié)議
TCP 協(xié)議(傳輸控制協(xié)議)是一種面向連接的、年面向字節(jié)流的、可靠的協(xié)議,它通過三次握手建立連接,并提供了流控制、擁塞控制、錯誤校驗等功能,保證數(shù)據(jù)傳輸?shù)目煽啃?。TCP 適用于對數(shù)據(jù)可靠性要求較高的應(yīng)用,如 Web 瀏覽器、郵件客戶端、文件傳輸?shù)取CP 協(xié)議的優(yōu)點在于它可以保證數(shù)據(jù)的可靠傳輸,但是由于它的連接建立和數(shù)據(jù)校驗等過程會增加網(wǎng)絡(luò)傳輸?shù)难舆t和開銷,因此在實時性要求較高的應(yīng)用中并不適用。
總的來說,TCP 適用于對數(shù)據(jù)可靠性要求較高的應(yīng)用,UDP 適用于實時性要求較高、數(shù)據(jù)傳輸量較小的應(yīng)用。在實際應(yīng)用中,根據(jù)應(yīng)用的不同需求,選擇合適的協(xié)議進行網(wǎng)絡(luò)傳輸,也可以根據(jù)需要將 TCP 和 UDP 協(xié)議結(jié)合使用,來達到更好的效果。
網(wǎng)絡(luò)字節(jié)序
網(wǎng)絡(luò)字節(jié)序(Network Byte Order)是一種統(tǒng)一的字節(jié)序,用于在計算機網(wǎng)絡(luò)中進行數(shù)據(jù)傳輸。由于不同的計算機可能使用不同的字節(jié)序(大小端),因此在網(wǎng)絡(luò)傳輸中,需要使用一種固定的字節(jié)序來確保數(shù)據(jù)的正確傳輸和解析。
網(wǎng)絡(luò)字節(jié)序采用的是大端字節(jié)序(Big-endian):將高位字節(jié)存放在內(nèi)存的低地址處,低位字節(jié)存放在內(nèi)存的高地址處。在網(wǎng)絡(luò)字節(jié)序中,所有數(shù)據(jù)類型(如整型、浮點型、字符型等)都采用相同的字節(jié)序,這樣就可以保證在不同的計算機上進行數(shù)據(jù)傳輸和解析時,不會出現(xiàn)字節(jié)序不一致的問題。
在 C 語言中,可以使用 htons、htonl、ntohs、ntohl 等函數(shù)來進行字節(jié)序轉(zhuǎn)換,其中,htons 和 htonl 函數(shù)用于將主機字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序,ntohs 和 ntohl 函數(shù)用于將網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換為主機字節(jié)序。
總之,網(wǎng)絡(luò)字節(jié)序是一種固定的字節(jié)序,用于在計算機網(wǎng)絡(luò)中進行數(shù)據(jù)傳輸和解析,保證了不同計算機之間數(shù)據(jù)的互通性和正確性。
??套接字編程??
套接字的分類
-
域間套接字(Inter-process Communication Socket,IPC Socket):也叫UNIX域套接字(Unix Domain Socket),是一種特殊的套接字類型,用于在同一臺計算機上不同進程之間進行通信,屬于進程間通信(IPC)機制的一種。它不需要經(jīng)過網(wǎng)絡(luò)協(xié)議棧的處理,因此傳輸速度非??欤膊粫艿骄W(wǎng)絡(luò)攻擊的影響。域間套接字使用一個本地的文件名作為地址,進程可以通過這個地址來建立連接并進行通信。
-
原始套接字(Raw Socket):也叫原始套接字類型,可以直接訪問傳輸層以下的網(wǎng)絡(luò)層協(xié)議,用于構(gòu)造和發(fā)送自定義的網(wǎng)絡(luò)協(xié)議數(shù)據(jù)包。原始套接字通常用于網(wǎng)絡(luò)協(xié)議分析、網(wǎng)絡(luò)攻擊、網(wǎng)絡(luò)安全等領(lǐng)域。使用原始套接字需要有足夠的權(quán)限,因為它可以直接訪問網(wǎng)絡(luò)層協(xié)議,對網(wǎng)絡(luò)安全帶來潛在的威脅。
-
網(wǎng)絡(luò)套接字(Internet Socket):也叫基于IP協(xié)議的套接字,是使用 TCP / IP 協(xié)議族進行通信的套接字類型。網(wǎng)絡(luò)套接字提供可靠的面向連接的通信服務(wù),支持 TCP 和 UDP 協(xié)議。在使用網(wǎng)絡(luò)套接字時,需要使用 IP 地址和端口號來標(biāo)識網(wǎng)絡(luò)中的進程,IP 地址用于標(biāo)識主機,端口號用于標(biāo)識主機上的應(yīng)用程序。網(wǎng)絡(luò)套接字使用 IPv4 或 IPv6 協(xié)議,其中 IPv4 協(xié)議使用 32 位地址,IPv6 協(xié)議使用 128 位地址,能夠更好地滿足互聯(lián)網(wǎng)的需求。
那 Linux 是如何設(shè)計這三類套接字的呢?Linux 系統(tǒng)為不同類型的套接字提供了統(tǒng)一的 socket API,并根據(jù)不同的地址族和套接字類型實現(xiàn)了不同的網(wǎng)絡(luò)協(xié)議和數(shù)據(jù)結(jié)構(gòu)。應(yīng)用程序只需要按照 API 規(guī)范來創(chuàng)建和使用套接字,就可以實現(xiàn)進程間通信和網(wǎng)絡(luò)通信等功能。
套接字的數(shù)據(jù)結(jié)構(gòu)
sockaddr、sockaddr_in 和 sockaddr_un 都是在 socket 編程中用于表示套接字地址的數(shù)據(jù)結(jié)構(gòu),它們之間的關(guān)系如下:
- sockaddr 是一個通用的套接字地址結(jié)構(gòu)體,它包含了地址族、地址信息等字段。在 socket 編程中,通常需要將sockaddr 類型的地址轉(zhuǎn)換成對應(yīng)的具體類型的地址結(jié)構(gòu)體,例如 sockaddr_in 或 sockaddr_un,才能方便地進行相關(guān)的操作。sockaddr 的結(jié)構(gòu)體定義如下:
struct sockaddr
{
sa_family_t sa_family; //地址族(AF_xxx)
char sa_data[14]; //14字節(jié)協(xié)議特定地址信息
};
-
sa_family 表示地址族,具體取值可以是 AF_UNIX、AF_INET、AF_INET6 等,sa_data 字段是協(xié)議特定的地址信息。在實際使用中,sockaddr 通常會被轉(zhuǎn)換為其他具體的地址結(jié)構(gòu)體,例如 sockaddr_in 或 sockaddr_un。
-
sockaddr_in 是 Internet 域套接字地址結(jié)構(gòu)體,它在sockaddr 的基礎(chǔ)上增加了 IPv4 地址和端口號字段。sockaddr_in 的結(jié)構(gòu)體定義如下:
struct sockaddr_in
{
sa_family_t sin_family; //地址族(AF_INET)
uint16_t sin_port; //16位端口號
struct in_addr sin_addr; //32位IPv4地址
char sin_zero[8]; //不使用的填充字段
};
- sockaddr_un 是 Unix 域套接字地址結(jié)構(gòu)體,它在sockaddr 的基礎(chǔ)上增加了一個路徑名字段。sockaddr_un的結(jié)構(gòu)體定義如下:
struct sockaddr_un
{
sa_family_t sun_family; //地址族(AF_UNIX)
char sun_path[108]; //文件路徑名
};
在實際的 socket 編程中,通常需要根據(jù)具體的網(wǎng)絡(luò)協(xié)議和地址族來選擇使用合適的套接字地址結(jié)構(gòu)體。例如,如果要使用 IPv4 協(xié)議進行通信,就需要使用 sockaddr_in 來表示 IPv4 地址和端口號。如果要實現(xiàn) Unix 域套接字通信,就需要使用 sockaddr_un 來表示文件路徑名。而sockaddr 則可以作為通用的套接字地址結(jié)構(gòu)體,用于一些通用的場景,例如進行套接字地址轉(zhuǎn)換等。
socket常見API
// 創(chuàng)建 socket 文件描述符 (TCP/UDP, 客戶端 + 服務(wù)器)
int socket(int domain, int type, int protocol);
// 綁定端口號 (TCP/UDP, 服務(wù)器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 開始監(jiān)聽socket (TCP, 服務(wù)器)
int listen(int socket, int backlog);
// 接收請求 (TCP, 服務(wù)器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立連接 (TCP, 客戶端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Linux 系統(tǒng)中,域間套接字、原始套接字和網(wǎng)絡(luò)套接字都是通過 socket API 來創(chuàng)建和使用的。
- 原始套接字:在 Linux 系統(tǒng)中,原始套接字通常需要 root 權(quán)限才能創(chuàng)建和使用。創(chuàng)建原始套接字時,應(yīng)用程序需要調(diào)用 socket() 函數(shù),指定地址族參數(shù)為 AF_PACKET,類型參數(shù)為 SOCK_RAW,然后再調(diào)用 bind() 函數(shù)將原始套接字綁定到指定的網(wǎng)卡上。通過原始套接字,應(yīng)用程序可以訪問傳輸層以下的網(wǎng)絡(luò)層協(xié)議,例如IP、ICMP、ARP等。
- 域間套接字:在 Linux 系統(tǒng)中,域間套接字通常被實現(xiàn)為文件系統(tǒng)中的一個文件。創(chuàng)建域間套接字時,應(yīng)用程序需要調(diào)用 socket() 函數(shù),指定地址族參數(shù)為 AF_UNIX,類型參數(shù)為 SOCK_STREAM 或 SOCK_DGRAM,然后再調(diào)用 bind() 函數(shù)將文件名綁定到套接字上。通過文件名,進程可以找到對應(yīng)的域間套接字,進行進程間通信。
- 網(wǎng)絡(luò)套接字:在 Linux 系統(tǒng)中,網(wǎng)絡(luò)套接字通常使用 IPv4 或 IPv6 協(xié)議進行通信。創(chuàng)建網(wǎng)絡(luò)套接字時,應(yīng)用程序需要調(diào)用 socket() 函數(shù),指定地址族參數(shù)為 AF_INET 或 AF_INET6,類型參數(shù)為 SOCK_STREAM 或 SOCK_DGRAM,然后再調(diào)用 bind() 函數(shù)將套接字綁定到指定的 IP 地址和端口號上。通過網(wǎng)絡(luò)套接字,應(yīng)用程序可以實現(xiàn)基于 TCP 或 UDP 協(xié)議的網(wǎng)絡(luò)通信。
??UDP服務(wù)器??
echo服務(wù)器
echo 服務(wù)器想要實現(xiàn)的功能是將客戶端發(fā)送過來的數(shù)據(jù),回顯給客戶端。
recvfrom 函數(shù)的最后兩個參數(shù)的含義:
為什么服務(wù)端進行綁定時,建議綁定全零的 IP 地址呢?
UdpServer.hpp
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP
#include "Log.hpp"
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define SIZE 1024
class UdpServer
{
public:
UdpServer(uint16_t port, std::string ip = "")
: _port(port)
, _ip(ip)
{}
void InitServer()
{
// 1. 創(chuàng)建套接字
// 前兩個參數(shù)已經(jīng)能確定是是UDP的網(wǎng)絡(luò)通信了,第三個參數(shù)設(shè)置為0即可
_socket = socket(AF_INET, SOCK_DGRAM, 0);
if(_socket < 0) // 創(chuàng)建套接字失敗
{
logMessage(FATAL, "errno:%d, strerror", errno, strerror(errno));
exit(2);
}
// 2. 綁定端口號(將用戶設(shè)置的ip和端口號與當(dāng)前的進程強綁定)
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
// 服務(wù)器的IP地址和端口號是要發(fā)給對方主機的,首先要發(fā)送到網(wǎng)絡(luò)
local.sin_port = htons(_port);
// 點分十進制的IP地址: "192.168.110.132"
// 每一個區(qū)域取值范圍是[0-255]: 1字節(jié) -> 4個區(qū)域
// 理論上,表示一個IP地址,其實4字節(jié)就夠了
// 需要將點分十進制字符串風(fēng)格的IP地址轉(zhuǎn)成4字節(jié)的二進制序列
// 4字節(jié)的二進制序列,還需要從主機序列轉(zhuǎn)為網(wǎng)絡(luò)序列
// 以上過程可以通過inet_addr函數(shù)來完成
// sin_addr.s_addr是4字節(jié)的二進制網(wǎng)絡(luò)序列
// INADDR_ANY表示發(fā)給這臺主機上的指定端口的數(shù)據(jù)都要交給UdpServer
// 如果綁定指定IP,就只能接收發(fā)給該IP的數(shù)據(jù)
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if(bind(_socket, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "errno:%d, strerror", errno, strerror(errno));
exit(3);
}
logMessage(NORMAL, "Init UdpServer Success!");
}
void StartServer()
{
// 網(wǎng)絡(luò)服務(wù)器是常駐進程,永遠(yuǎn)不會退出,除非掛掉了
char buffer[SIZE];
while(true)
{
struct sockaddr_in peer; // 輸出型參數(shù)
bzero(&peer, sizeof(peer)); // 將比特位全部置為0
socklen_t len = sizeof(peer); // 輸入輸出型參數(shù)
ssize_t s = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
buffer[s] = '\0';
uint16_t clientPort = ntohs(peer.sin_port);
std::string clientIP = inet_ntoa(peer.sin_addr);
printf("clientIP:%s clientPort:%d# %s\n", clientIP.c_str(), clientPort, buffer);
}
// 寫回數(shù)據(jù)
sendto(_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
}
}
~UdpServer()
{
if(_socket >= 0) close(_socket); // 關(guān)閉文件描述符
}
private:
uint16_t _port;
std::string _ip;
int _socket;
};
#endif
功能說明:
- UdpServer 的構(gòu)造函數(shù)是指定服務(wù)器的 IP 地址和端口號。
- InitServer 接口的功能是創(chuàng)建套接字和綁定端口號。
- StartServer 接口的功能是接收客戶端的數(shù)據(jù)并將數(shù)據(jù)寫回給客戶端。
UdpServer.cc
#include "UdpServer.hpp"
#include <memory>
static void Usage(std::string proc)
{
std::cout << "\nUsage: " << proc << "Port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<UdpServer> ptr(new UdpServer(port));
ptr->InitServer();
ptr->StartServer();
return 0;
}
UdpClient.cc
為什么客戶端不能顯式綁定端口號呢?
客戶端通常不需要顯式綁定端口號,因為客戶端只需要連接到服務(wù)端的指定端口號即可。當(dāng)客戶端向服務(wù)端發(fā)起連接請求時,操作系統(tǒng)會自動為客戶端分配一個隨機的空閑端口號,并在連接請求中包含該端口號信息,以便服務(wù)端能夠返回數(shù)據(jù)給正確的客戶端端口。
這種自動分配端口號的機制稱為“臨時端口號”或“短暫端口號”,它的使用使得客戶端和服務(wù)端的通信更加簡單和可靠。同時,如果客戶端需要綁定特定的端口號,則必須要保證該端口號未被其他應(yīng)用程序占用,否則會導(dǎo)致連接失敗。因此,客戶端通常不需要顯式綁定端口號。
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
static void Usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " ServerIP ServerPort" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 1. 創(chuàng)建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
std::cerr << "Socket Error!" << std::endl;
exit(2);
}
char buffer[1024];
std::string message;
struct sockaddr_in server;
memset(&server, sizeof(server), 0);
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(atoi(argv[2]));
// 客戶端一般不需要顯示地綁定端口號,如果客戶端綁定了一個固定的
// IP和端口號,那么其他客戶端也綁定了這個端口號,這時候客戶端就會
// 綁定失敗。所以客戶端一般不需要顯式地綁定端口號,而是讓操作系統(tǒng)
// 隨機選擇一個端口號進行綁定。什么時候進行綁定,當(dāng)客戶端首次發(fā)送
// 數(shù)據(jù)給服務(wù)器時,操作系統(tǒng)會自動進行客戶端的端口綁定
while(true)
{
std::cout << "Please Enter Your Message: ";
std::getline(std::cin, message);
if(message == "quit") break;
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
// 接收數(shù)據(jù):當(dāng)前的客戶端有可能是別的主機的服務(wù)端
ssize_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
if(s > 0)
{
buffer[s] = '\0';
std::cout << "server# " << buffer << std::endl;
}
}
close(sock);
return 0;
}
功能測試以及注意事項
netstat 是一個用于顯示和分析 Linux 系統(tǒng)網(wǎng)絡(luò)連接狀態(tài)的命令行工具。下面是常用的 netstat 參數(shù)的含義:
- -a 顯示所有的網(wǎng)絡(luò)連接狀態(tài),包括 TCP、UDP 和 UNIX域套接字。
- -t 只顯示 TCP 連接狀態(tài)。
- -u 只顯示 UDP 連接狀態(tài)。
- -n 不進行主機名和端口號的解析,使用數(shù)字形式來顯示地址和端口號。
- -p 顯示與連接相關(guān)聯(lián)的程序名稱和進程ID。
- -e 顯示與連接相關(guān)聯(lián)的擴展信息,如 TCP 的 SACK 和 Windows 擴展選項。
- -c 持續(xù)輸出網(wǎng)絡(luò)連接狀態(tài),每隔一秒鐘更新一次。
- -l 僅顯示監(jiān)聽狀態(tài)的連接。
使用 netstat 命令時,可以根據(jù)具體需要組合使用這些參數(shù),以便查看系統(tǒng)的網(wǎng)絡(luò)連接狀態(tài)。例如,使用 netstat -tunap 可以同時顯示所有 TCP 和 UDP 連接狀態(tài),并顯示與每個連接相關(guān)聯(lián)的程序名稱和進程 ID。
什么是本地環(huán)回?
本地環(huán)回(loopback)是一種計算機網(wǎng)絡(luò)通信的機制,它允許計算機通過一個虛擬的網(wǎng)絡(luò)接口與自己通信。在 TCP / IP 協(xié)議中,本地環(huán)回地址被定義為 127.0.0.1,也稱為回環(huán)地址。
當(dāng)計算機通過回環(huán)地址發(fā)送數(shù)據(jù)包時,操作系統(tǒng)會將這些數(shù)據(jù)包送回到發(fā)送者本身,而不是發(fā)送到網(wǎng)絡(luò)上。這種機制使得計算機可以自我測試和調(diào)試,同時也可以用于本地服務(wù)的訪問和通信。
在網(wǎng)絡(luò)編程中,本地環(huán)回地址可以用來測試和調(diào)試客戶端和服務(wù)器程序。例如,可以將客戶端程序連接到回環(huán)地址的某個端口上,以模擬連接到遠(yuǎn)程服務(wù)器的情況。同樣地,服務(wù)器程序也可以監(jiān)聽回環(huán)地址上的某個端口,以模擬接收來自遠(yuǎn)程客戶端的請求。
總之,本地環(huán)回是一種非常有用的網(wǎng)絡(luò)通信機制,它使得計算機可以在不涉及真實網(wǎng)絡(luò)的情況下進行自我測試和調(diào)試,以及本地服務(wù)的訪問和通信。
Linux系統(tǒng)中常用的文件傳輸工具:rz(收) 和 sz(發(fā)),通過 sz 指令就可以將 Linux 下的程序發(fā)到 Windows 系統(tǒng)上了,rz 指令可以將 Windows 上的文件傳給 Linux系統(tǒng)。
日志功能
#pragma once
#include <cstdio>
#include <cstdarg>
#include <string>
#include <iostream>
#include <ctime>
// 日志等級
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOGFILE "./ThreadPool.log"
const char* levelMap[] =
{
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
void logMessage(int level, const char* format, ...)
{
// 只有定義了DEBUG_SHOW,才會打印debug信息
// 利用命令行來定義即可,如-D DEBUG_SHOW
#ifndef DEBUG_SHOW
if(level == DEBUG) return;
#endif
char stdBuffer[1024]; // 標(biāo)準(zhǔn)部分
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", levelMap[level], timestamp);
char logBuffer[1024]; // 自定義部分
va_list args; // va_list就是char*的別名
va_start(args, format); // va_start是宏函數(shù),讓args指向參數(shù)列表的第一個位置
// vprintf(format, args); // 以format形式向顯示器上打印參數(shù)列表
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args); // va_end將args弄成nullptr
// FILE* fp = fopen(LOGFILE, "a");
printf("%s%s\n", stdBuffer, logBuffer);
// fprintf(fp, "%s%s\n", stdBuffer, logBuffer); // 向文件中寫入日志信息
// fclose(fp);
}
Makefile
.PHONY:all
all:UdpClient UdpServer
UdpClient:UdpClient.cc
g++ -o $@ $^ -std=c++11
UdpServer:UdpServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f UdpClient UdpServer
指令服務(wù)器
指令服務(wù)器要實現(xiàn)的功能就是服務(wù)端將客戶端發(fā)送過來的數(shù)據(jù)當(dāng)做 Linux 指令,然后在服務(wù)端執(zhí)行該指令并將指令的執(zhí)行結(jié)果返回給客戶端。
要實現(xiàn)指令服務(wù)器,我們需要借助 popen 函數(shù)。
popen 函數(shù)是 C 語言標(biāo)準(zhǔn)庫中的一個函數(shù),它可以創(chuàng)建一個進程并與之建立一個管道。該管道可以實現(xiàn)進程之間的通信,父進程可以向子進程發(fā)送數(shù)據(jù),并讀取子進程的輸出。popen 函數(shù)原型如下:
FILE *popen(const char *command, const char *type);
其中,command 參數(shù)是要執(zhí)行的命令或程序,type 參數(shù)是打開的模式,可以是 “r”(只讀模式)或 “w”(只寫模式)。函數(shù)返回一個文件指針,可以像讀寫文件一樣操作管道。
例如,以下代碼創(chuàng)建一個進程并向其發(fā)送數(shù)據(jù),并從管道中讀取子進程的輸出:
#include <stdio.h>
int main()
{
FILE *fp;
char buffer[1024];
fp = popen("ls -l", "r");
if (fp == NULL)
{
printf("Error: Failed to execute command.\n");
return -1;
}
while (fgets(buffer, sizeof(buffer), fp) != NULL)
{
printf("%s", buffer);
}
pclose(fp);
return 0;
}
該程序打開一個名為 ls -l 的進程,并將其標(biāo)準(zhǔn)輸出讀取到一個緩沖區(qū)中,最后輸出到屏幕上。需要注意的是,popen 函數(shù)可能會存在一些安全隱患,因為在打開進程時會執(zhí)行一個命令字符串。因此,在使用該函數(shù)時需要謹(jǐn)慎,避免輸入不受信任的命令字符串。
注:只需要改寫 echo 服務(wù)器的 StartServer 接口即可。strcaststr 是查找子串的函數(shù),它的查找是忽略大小寫的。
void StartServer()
{
// 網(wǎng)絡(luò)服務(wù)器是常駐進程,永遠(yuǎn)不會退出,除非掛掉了
char buffer[SIZE];
while(true)
{
struct sockaddr_in peer; // 輸出型參數(shù)
bzero(&peer, sizeof(peer)); // 將比特位全部置為0
socklen_t len = sizeof(peer); // 輸入輸出型參數(shù)
char ret[256];
std::string cmdRet; // 命令的執(zhí)行結(jié)果
ssize_t s = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
buffer[s] = '\0';
// 過濾危險指令
if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr)
{
std::string errMessage = "哎呦,你干嘛!";
std::cout << errMessage << buffer << std::endl;
sendto(_socket, errMessage.c_str(), errMessage.size(), 0, (struct sockaddr*)&peer, len);
continue;
}
// buffer中的內(nèi)容看做指令
FILE* fp = popen(buffer, "r");
if(fp == nullptr) // 打開文件失敗
{
logMessage(ERROR, "errno:%d strerror:%s\n", errno, strerror(errno));
continue;
}
// 從文件中讀取指令執(zhí)行結(jié)果
while(fgets(ret, sizeof(ret), fp) != nullptr)
{
cmdRet += ret;
}
fclose(fp);
uint16_t clientPort = ntohs(peer.sin_port);
std::string clientIP = inet_ntoa(peer.sin_addr);
printf("clientIP:%s clientPort:%d# %s\n", clientIP.c_str(), clientPort, buffer);
}
// 將指令的執(zhí)行結(jié)果發(fā)送給客戶端
sendto(_socket, cmdRet.c_str(), cmdRet.size(), 0, (struct sockaddr*)&peer, len);
}
}
簡易的公共聊天室
簡易版的公共聊天室的主要功能是將一個用戶發(fā)的信息同步到其他用戶中去,那么這就以為這一個用戶既要發(fā)信息,也要接收其他用戶發(fā)的信息,而這兩個過程可以通過寫線程和讀線程來模擬。而服務(wù)端則需要將已經(jīng)向服務(wù)端發(fā)過消息的用戶記錄下來,以便后續(xù)將一個用戶發(fā)的消息同步給其他用戶。
UdpServer.hpp
class UdpServer
{
public:
void StartServer()
{
// 網(wǎng)絡(luò)服務(wù)器是常駐進程,永遠(yuǎn)不會退出,除非掛掉了
char buffer[SIZE];
while(true)
{
struct sockaddr_in peer; // 輸出型參數(shù)
bzero(&peer, sizeof(peer)); // 將比特位全部置為0
socklen_t len = sizeof(peer); // 輸入輸出型參數(shù)
char ret[256];
char key[64];
ssize_t s = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
buffer[s] = '\0';
uint16_t clientPort = ntohs(peer.sin_port);
std::string clientIP = inet_ntoa(peer.sin_addr);
// key是用戶的IP地址加上端口號
snprintf(key, sizeof(key), "IP:%s Port:%d", clientIP.c_str(), clientPort);
logMessage(NORMAL, "key: %s", key);
// 查找用戶是否注冊過
auto it = _users.find(key);
// 需要將第一次發(fā)消息向服務(wù)器的用戶保存起來
if(it == _users.end())
{
logMessage(NORMAL, "Add A New User: %s", key);
_users[key] = peer;
}
}
// 給所有的用戶發(fā)送該消息
for(auto& it : _users)
{
std::string sendMessage = key;
sendMessage += "# ";
sendMessage += buffer;
logMessage(NORMAL, "Push A Message To All Users");
sendto(_socket, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr*)&(it.second), sizeof(it.second));
}
}
}
private:
std::unordered_map<std::string, struct sockaddr_in> _users;
};
Thread.hpp
#pragma once
#include <string>
#include <pthread.h>
#include <cstdio>
typedef void*(*func_t)(void*);
class ThreadData
{
public:
void *_args; // 線程執(zhí)行例程的參數(shù)
std::string _name; // 線程名
pthread_t _tid; // 線程ID
};
class Thread
{
public:
Thread(int num, func_t callBack, void* args)
: _func(callBack)
{
char nameBuffer[64];
snprintf(nameBuffer, sizeof nameBuffer, "Thread %d", num);
_data._args = args;
_data._name = nameBuffer;
}
// 創(chuàng)建線程
void Create()
{
pthread_create(&_data._tid, nullptr, _func, (void*)&_data);
}
// 等待線程
void Join()
{
pthread_join(_data._tid, nullptr);
}
// 返回線程的名字
std::string Name()
{
return _data._name;
}
~Thread()
{}
private:
func_t _func; // 線程的執(zhí)行例程
ThreadData _data; // 線程的屬性
};
注:Thread.hpp 是對線程進行了封裝,詳細(xì)介紹可以參考這篇博客:線程池的實現(xiàn)。
UdpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#include "Thread.hpp"
uint16_t ServerPort = 0;
std::string ServerIP;
static void Usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " ServerIP ServerPort" << std::endl;
}
// 寫線程
static void* UdpSend(void* args)
{
ThreadData* td = (ThreadData*)args;
int sock = *(int*)td->_args;
std::string message;
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ServerIP.c_str());
server.sin_port = htons(ServerPort);
while(true)
{
std::cerr << "Please Enter Your Message: ";
std::getline(std::cin, message);
if(message == "quit") break;
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
}
return nullptr;
}
// 讀線程
static void* UdpRecv(void* args)
{
ThreadData* td = (ThreadData*)args;
int sock = *(int*)td->_args;
std::string threadName = td->_name;
char buffer[1024];
while(true)
{
memset(buffer, sizeof buffer, 0);
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
if(s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}
}
return nullptr;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 1. 創(chuàng)建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
// 細(xì)節(jié):使用cerr標(biāo)準(zhǔn)錯誤,這樣可以為后面的重定向做準(zhǔn)備
std::cerr << "Socket Error!" << std::endl;
exit(2);
}
ServerIP = argv[1];
ServerPort = atoi(argv[2]);
// 讀線程和寫線程都不會修改端口號,所以不需要進行加鎖保護
// 讀寫進程使用的是同一個端口號sock,sock是文件描述符
// 說明Udp是全雙工的(可以同時進行讀寫且不受干擾)
std::unique_ptr<Thread> Sender(new Thread(1, UdpSend, (void*)&sock));
std::unique_ptr<Thread> Recver(new Thread(2, UdpRecv, (void*)&sock));
// 創(chuàng)建寫線程和讀線程
Sender->Create();
Recver->Create();
// 等待寫線程和讀線程
Sender->Join();
Recver->Join();
close(sock);
return 0;
}
注:mkfifo 創(chuàng)建命名管道,并將用戶接收到的信息重定向到管道文件中,然后用另一個會話從管道文件中讀取該用戶收到的信息。
什么是全雙工通信方式?為什么 Udp 協(xié)議是全雙工的?
全雙工通信是指在通信的雙方可以同時進行發(fā)送和接收數(shù)據(jù)的通信方式。這種通信方式可以實現(xiàn)同時雙方交換信息,從而提高通信效率。
UDP協(xié)議是一個無連接的、面向數(shù)據(jù)報的協(xié)議,同時它也是一種全雙工的通信方式,即在同一時刻,它允許數(shù)據(jù)的發(fā)送和接收。
Windows 版本的客戶端文章來源:http://www.zghlxwxcb.cn/news/detail-408882.html
#include <WinSock2.h>
#include <iostream>
#include <string>
#include <thread>
#include <memory>
#include <cstring>
using namespace std;
#pragma comment(lib,"ws2_32.lib") //固定用法
uint16_t serverport = 8080;
// serverip需要替換成自己的云服務(wù)器的公網(wǎng)IP
std::string serverip = "xxx.xxx.xxx.xxx";
void Sender(SOCKET clientSocket)
{
sockaddr_in dstAddr;
dstAddr.sin_family = AF_INET;
dstAddr.sin_port = htons(serverport);
dstAddr.sin_addr.S_un.S_addr = inet_addr(serverip.c_str());
while (true)
{
std::string message;
std::cerr << "請輸入# ";
std::getline(std::cin, message);
if (message == "quit") break;
sendto(clientSocket, message.c_str(), (int)message.size(), 0, (sockaddr*)&dstAddr, sizeof(dstAddr));
message.clear();
}
}
void Recver(SOCKET clientSocket)
{
char buffer[1024];
while (true)
{
memset(buffer, sizeof buffer, 0);
struct sockaddr_in temp;
int len = sizeof(temp);
int s = recvfrom(clientSocket, buffer, sizeof buffer, 0, (sockaddr*)&temp, &len);
if (s > 0)
{
buffer[s] = '\0';
cout << "server echo# " << buffer << endl;
}
}
}
int main()
{
// windows 獨有的
WSADATA WSAData;
WORD sockVersion = MAKEWORD(2, 2);
if (WSAStartup(sockVersion, &WSAData) != 0)
return 0;
SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
if (INVALID_SOCKET == clientSocket)
{
cout << "socket error!";
return 0;
}
unique_ptr<thread> send(new thread(Sender, clientSocket));
unique_ptr<thread> recv(new thread(Recver, clientSocket));
send->join();
recv->join();
// windows 獨有
closesocket(clientSocket);
WSACleanup();
return 0;
}
??總結(jié)??
本篇博客主要講解了什么是源IP地址和目的IP地址、什么是端口號、PID和端口號的區(qū)別、什么是套接字、簡單認(rèn)識UDP和TCP協(xié)議、什么是網(wǎng)絡(luò)字節(jié)序、套接字的分類、數(shù)據(jù)結(jié)構(gòu)和常見API以及使用UDP協(xié)議來進行編寫服務(wù)端和客戶端等。以上就是本篇博客的全部內(nèi)容了,如果大家覺得有收獲的話,可以點個三連支持一下!謝謝大家啦!??????文章來源地址http://www.zghlxwxcb.cn/news/detail-408882.html
到了這里,關(guān)于【Linux】揭開套接字編程的神秘面紗(上)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!