Linux C/C++ UDP 網路通訊
昨晚 Vv 想讓我給她講講網路編程,於是我就傻乎乎的帶她入了門...
以下內容為講課時製作的筆記~
1. socket() 函數
1.1 頭文件
#include<sys/socket.h>
1.2 函數參數
示例:int socket(int domain, int type, int protocol){...}
-
domain:設置協議域(協議族)
- AF_INET:IPV4
- AF_INET6:IPV6
- \(\cdots\)
協議族決定了 socket 的地址類型,在通訊中必須採用對應類型的地址
-
type:指定 socket 類型
- SOCKET_STREAM:流式 socket,針對於面向連接的 TCP 服務應用
- SOCKET_DGRAM:數據報式 socket,針對於無連接的 UDP 服務應用
- \(\cdots\)
-
protocal:指定協議
- \(0\):自動選擇第二個參數類型對應的傳輸協議
- IPPROTO_TCP:TCP傳輸協議
- IPPROTO_UDP:UDP傳輸協議
- \(\cdots\)
type 和 protocal 不能隨意組合,如 SOCKET_STREAM 不能和 IPPROTO_UDP 組合
1.3 返回值
示例:int sock_fd = socket(AF_INET, SOCKET_DGRAM, 0);
-
sock_fd = -1:套接字創建失敗 -
sock_fd = x(x >= 0):套接字創建成功,返回套接字的文件描述符(索引)套接字描述符是一個整數類型的值。每個進程的進程空間里都有一個套接字描述符表,該表中存放著套接字描述符和套接字數據結構的對應關係。該表中有一個欄位存放新創建的套接字的描述符,另一個欄位存放套接字數據結構的地址,因此根據套接字描述符就可以找到其對應的套接字數據結構。每個進程在自己的進程空間里都有一個套接字描述符表但是套接字數據結構都是在作業系統的內核緩衝里。
1.4 Socket是什麼?
socket是對TCP/IP協議簇的封裝,它的出現只是使得程式設計師更方便地使用TCP/IP協議棧而已。socket本身並不是協議,它是應用層與TCP/IP協議族通訊的中間軟體抽象層,是一組調用介面(TCP/IP網路的API函數)
2. bind()函數
2.1 sockaddr
#include<arpa/inet.h>
struct sockaddr{
sa_family_t sin_family; // 協議族
char sa_data[14]; // 14 個位元組,包含套接字中的目標地址和目標埠資訊
};
2.2 sockaddr_in
#include<arpa/inet.h> // 或 #include<netinet/in.h>
struct in_addr{
In_addr_t s_addr; // 32位 IPv4 地址
};
struct sockaddr_in{
sa_family_t sin_family; // 協議族
uint16_t sin_port; // 16位 TCP/UDP 埠號 (埠號最大是 65535 = 2^16 - 1)
struct in_addr; // 32位 IP 地址
char sin_zero[8]; // 不使用 (為了讓sockaddr與sockaddr_in兩個數據結構保持大小相同而保留的空位元組)
};
- sin_port 和 sin_addr 都必須是網路位元組序(NBO),一般可視化的數字都是主機位元組序(HBO)。
- sockaddr_in 和 sockaddr 是並列的結構,指向 sockaddr_in 的結構體的指針也可以指向 sockadd 的結構體,並代替它。
2.3 函數參數
示例:int bind(sock_fd, const struct sockaddr* address, socklen_t address_len);
-
sock_fd:套接字描述符
-
address:sockaddr結構指針,該結構中包含了要綁定的地址和埠號
-
address_len:address緩衝區的長度
- socklen_t 即 unsigned int
- sizeof 的返回值也是 unsigned int
2.4 返回值
示例:
// 綁定 ip port
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9123); // htons 主機位元組序轉網路位元組序
// 方法1:
// INADDR_ANY 是通配地址,即本機所有 ip 都綁定上。 INADDR_ANY 轉換過來就是0.0.0.0
inet_pton(AF_INET, INADDR_ANY, &addr.sin_addr.s_addr);
// 方法2:
// inet_addr()作用是將一個IP字元串轉化為一個網路位元組序的整數值,用於sockaddr_in.sin_addr.s_addr。
addr.sin_addr.s_addr = inet_addr("192.168.0.115");
int res = bind(sock_fd, (struct sockaddr *) &addr, sizeof(addr));
res = 0:綁定成功res = -1:綁定失敗
2.5 作用
將 addr 指向的 sockaddr 結構體中描述的一些屬性(IP地址、埠號、地址簇)與 socket 套接字綁定,也叫給套接字命名。
調用 bind() 後,就為 socket 套接字關聯了一個相應的地址與埠號,即發送到該地址該埠的數據可通過 socket 讀取和使用。當然也可通過該 socket 發送數據到指定目的。
對於Server,bind()是必須要做的事情,伺服器啟動時需要綁定指定的埠來提供服務(以便於客戶向指定的埠發送請求),對於伺服器 socket 綁定地址,一般而言將 IP 地址賦值為 INADDR_ANY(該宏值為0),即無論發送到系統中的哪個 IP 地址(當伺服器有多張網卡時會有多個 IP 地址)的請求都採用該 socket 來處理,而無需指定固定 IP。
對於 Client,一般而言無需主動調用 bind(),一切由作業系統來完成。在發送數據前,作業系統會為套接字隨機分配一個可用的埠,同時將該套接字和本地地址資訊綁定。
關於套接字更詳細的使用,可參考://github.com/qiyu56/network/tree/master/udp
3. sendto() 函數
3.1 函數參數
示例:int sendto(int sock_fd, const void *buf, int len, int flags, const struct sockaddr *address, socklen_t address_len);
-
sock_fd:套接字描述符
-
void *buf:UDP 數據報快取區(包含待發送數據)
-
void* 指針可以指向任意類型的數據:
void *p; int *a; p = a; // a = (int *)p; -
UDP 數據報快取區:
- sendto 把數據放在 sendbuf(緩衝區),通知作業系統來取
- 作業系統在適當的時候過來取數據,並發到網路
這意味著:存入數據和發送數據存在時間差(非同步的),如果存入數據太快太多,緩衝區會滿。緩衝區滿的處理:
- 知道緩衝區有剩餘空間(阻塞)
- 新發送的數據沒有存入緩衝區(直接丟掉)
丟包對 UDP 來說是很正常,在使用 UDP 時就應該允許丟包
-
-
len:UDP數據報的長度
-
flags:調用方式標誌位(一般設置為 \(0\),先不掌握)
-
sockaddr *address:sockaddr結構指針,該結構中包含了要發送的地址和埠號
-
address_len:address緩衝區的長度
- socklen_t 即 unsigned int
- sizeof 的返回值也是 unsigned int
3.2 返回值
示例:
char buf[128] = "";
fgets(buf, sizeof(buf) , stdin);
int res = sendto(sock_fd , buf , strlen(buf) , 0, (struct sockaddr *) &server_addr, sizeof(server_addr));
res = x:發送成功,\(x\) 為發送出去的字元數res = -1:發送失敗
3.3 作用
把 UDP 數據報發給指定地址。
4. revcfrom() 函數
4.1 函數參數
示例:recvfrom(int socke_fd, const void *buf, int len, int flags, struct sockaddr *address, socklen_t *address_len)
-
sock_fd:套接字描述符
-
void *buf:UDP 數據報快取區(包含所接收的數據)
-
UDP 數據報快取區:
- 作業系統不停把從網路上接收數據,快取在 recvbuf(緩衝區) 里
- recvfrom從快取區里接收數據
這意味著:不論你是否去取數據,操作總是把數據收下來存好,recfrom是從recvbuf里取走現成的數據,如果不及時取走,則緩衝區會滿。緩衝區滿的處理:
- 新的數據不被接收
- 刪除緩衝區里的現有的數據,存放新的數據。
-
-
len:UDP數據報的長度
-
flags:調用方式標誌位(一般設置為 \(0\),先不掌握)
-
sockaddr *address:sockaddr結構指針,該結構中包含了發送方的地址和埠號(可以為 NULL)
-
address_len:socklen_t 指針,指向了 address 結構體的長度(可以為 NULL)
4.2 返回值
示例:
char buf[128] = "";
int recv_len = recvfrom(sock_fd, buf, sizeof(buf), 0, (struct sockaddr*)&client_addr, &client_len);
recv_len = x:接收成功,\(x\) 為接收到的字元數res = -1:接收失敗
4.3 作用
接收發送方的網路數據。
5. 伺服器程式碼與客戶端程式碼
Server.cpp
#include<bits/stdc++.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main(int argc , char *argv[]){
cout << "Server:\n";
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_fd < 0) {
perror("socket 創建失敗");
return 0;
}
cout << "socket 創建成功!\n";
// 綁定 ip port
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9123);
// inet_pton(AF_INET, "192.168.0.111", &addr.sin_addr.s_addr);
addr.sin_addr.s_addr = inet_addr("192.168.0.115"); //INADDR_ANY 通配地址,即本機所有 ip 都綁定上。 INADDR_ANY 轉換過來就是0.0.0.0
int res = bind(sock_fd, (struct sockaddr *) &addr, sizeof(addr));
if(res < 0) {
perror("綁定失敗");
close(sock_fd);
return 0;
}
cout << "socket 綁定(命名)成功!\n";
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
while(1){
char buf[128] = "";
int recv_len = recvfrom(sock_fd, buf, sizeof(buf), 0, (struct sockaddr*)&client_addr, &client_len);
printf("來自 ip 地址為 %s 埠號為 %d 的資訊:%s 資訊的總長度為 %d\n" , inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buf, recv_len);
sendto(sock_fd, buf, recv_len, 0, (struct sockaddr*)&client_addr, sizeof(client_addr));
}
close(sock_fd);
return 0;
}
Client.cpp
#include<bits/stdc++.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main(int argc, char *argv[]){
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9123); // 伺服器埠
inet_pton(AF_INET, "192.168.0.115", &server_addr.sin_addr.s_addr);
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_fd < 0)
perror("");
while(1){
char buf[128] = "";
cin.getline(buf , sizeof(buf));
int res = sendto(sock_fd , buf , strlen(buf) , 0, (struct sockaddr *) &server_addr, sizeof(server_addr));
char read_buf[128] = "";
recvfrom(sock_fd, read_buf, sizeof(read_buf), 0, NULL, NULL);
printf("共發送 %d 個字元數\n" , res);
}
close(sock_fd);
return 0;
}

