文章目錄
- Socket編程UDP
- UDP接口的使用鋪墊
- socket
- recvform && sendto
- bind
- 字節序轉化使用(Tips)
- 實踐部分
- version_1@echo_server
- version_2@dict_server
- version_3@chat_server
Socket編程UDP
在了解了相關的網絡基礎知識后,我們不會像學系統知識一樣,先學原理,再講應用。
我們先先不選擇學習網絡原理。我們先來試著學習一下使用接口來完成一些簡單地實踐。
在這個部分中,我們不只使用網絡相關知識,而是結合前面系統部分學習時,完成的一些組件代碼來使用!這些我們后面會見到的!
UDP接口的使用鋪墊
先說一下,有了網絡基礎知識 + 系統部分的知識。其實我們只需要了解一下UDP相關接口的使用,其實就能較為熟練地學習如何使用。下面,我們將把網絡中將用的接口進行簡單講解。
socket
NAMEsocket - create an endpoint for communication //打開通信的一端SYNOPSIS#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int socket(int domain, int type, int protocol);
這個文件,是用來創建套接字的!我們在網絡基礎部分講過,兩個進程的通信是基于套接字(端口號 + ip)的!具體的創建,就是用這個接口。
第一個參數domain是選擇使用什么域來進行通信:
這里我們記住是選擇AF_INET進行網絡通信即可!
第二個參數type是選擇使用什么方式傳輸,比如TCP/UDP:
字符流 -> SOCK_STREAM -> TCP
數據包 -> SOCK_DGRAM -> UDP
第三個參數protocol是要我們選擇協議,默認給0就是TCP/IP了,記住即可!
返回值:
成功,返回一個file descriptor,即文件描述符!即使我們不知道這個網絡通信的原理,但是我們至少可以直到,當前進程的文件描述符表定會有一個位置指向socket文件!
所以,我們就把它當成特殊的網絡文件來使用!這符合 Linux下一切皆文件!
recvform && sendto
我們先來看著兩個函數的相關信息,它們具有較強相似性:
recvform:
NAMErecv, recvfrom, recvmsg - receive a message from a socketSYNOPSIS#include <sys/types.h>#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);RETURN VALUEThese calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error.
sendto:
NAMEsend, sendto, sendmsg - send a message on a socketSYNOPSIS#include <sys/types.h>#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);RETURN VALUEOn success, these calls return the number of bytes sent. On error, -1 is returned, and errno is set appropriately.
見名思意:
recvform是從套接字文件中獲取內容;sendto是向套接字文件中發送內容。
參數解釋:
-
int sockfd,就是對應的套接字文件!這個不需要解釋。
-
const void *buf,這個就是一個緩沖區,recvfrom把從套接字文件中讀到的內容讀取到該緩沖區。sendto將該緩沖區內的內容發送到套接字文件。
-
size_t len,即緩沖區長度。
-
int flags,這個是選擇是否阻塞讀取/發送的。默認給0就是阻塞。也就是說,recvfrom讀不到內容就會阻塞,sendto沒有內容發送也會阻塞。這個我們在系統那里也見過。
-
struct sockaddr,這個其實我們早在網絡基礎部分的時候,就已經講到過這個。我們說了,設計socket網絡通信的時候,為了能夠讓網絡通信和本地通信公用一套接口,所以設計了這么個c語言版的基類!
所以,如果需要收發消息,其實對方進程的相關信息:ip、端口號、通信協議,都會在對應的結構體上體現出來。所以,未來在使用socket通信的時候,如何知道或者發送自己的相關信息,就是靠著這個結構體sockaddr_in或者sockaddr_un,強轉類型為sockaddr對應的地址變量后,然后進行相關操作!
6.socklen_t *addrlen和socklen_t addrlen:
6.1. 對于recvfrom收消息來說,參數是socklen_t *addrlen,這很明顯是一個指針地址變量!所以,是需要我們把sockaddr_in的地址傳進去給第五個參數,然后需要定義一個變量指明該結構體大小,傳入地址給第六個參數。
Tips:因為該變量是輸出型參數,實際上它會返回真正讀到的字節數(因為網絡傳輸可能丟包)
6.2. 對于sendto發消息來說,這個就沒什么好說的了,本身數據就在,直接傳對應的大小即可
bind
NAMEbind - bind a name to a socketSYNOPSIS#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);RETURN VALUEOn success, zero is returned. On error, -1 is returned, and errno is set appropriately.
沒看錯,這里還有一個接口叫做bind,但是,這個不是c++11后提出的std::bind!
這個接口最重要的就是,將套接字文件和對應的進程的IP地址和端口號(在結構體sockaddr_in內存儲了),綁定到對應的套接字文件中!
??bind的作用??:
顯式綁定:bind()將套接字固定到特定IP+端口,成為該地址的唯一接收者。
未綁定時:系統在首次發送數據時自動分配隨機端口(IP默認為所有本地接口)
這里理解一下就好,實在看不懂的話等后續講解的時候再說一遍如何使用。現在只需要學會使用即可,原理等后期再來講解!
字節序轉化使用(Tips)
這個在網絡基礎中也是提到了。這里再多提醒一下,因為后續的實踐中,必然是有從網絡序列轉主機序列的內容,也有主機序列轉網絡序列的!所以,這些是必須使用的!
實踐部分
version_1@echo_server
源碼:version_1@echo_server
第一個版本,我們希望實現一種功能:
即有一個服務器,然后其余所有的進程都可以給其發消息,然后服務器處理后再轉發給對應的進程,這樣子就起到了一個回顯服務器的效果!
所有的過程都已經在代碼的注釋中展現出來了。
version_2@dict_server
源碼:version_2@dict_server
第二個版本,其實就是進行一次解耦合!讓第一個版本中對于信息回顯的處理模塊,轉化為服務器調用詞典翻譯模塊!其實主要的邏輯和第一個版本差別不大。
只不過是引入多了一個模塊,讓詞典從對應的配置文件中讀取對應信息,然后服務器進行調用后再進行轉發結果給請求該服務的進程!
version_3@chat_server
源碼:version_3@chat_server
第三個版本我們來詳細說明一下:
第三個版本我們希望做到一個群聊轉發功能,即服務器在接收到某個客戶端發送來的消息后,要轉發給所有處于在線的用戶。
1.我們這里規定:
默認第一次發送消息的就是要加入群聊的。服務器接收到消息之后,要怎么樣才能轉發給所有的在線用戶呢?-> 添加一層路由層,用于管理在線用戶(組織描述)和消息轉發!
2.但是,我們覺得效率太低了,所以希望的是,服務器將收到的信息推給后端線程池,讓線程池自行調用路由轉發功能!所以引入了線程池。
3.此前版本1、2寫的客戶端使用代碼是有問題的!因為強行規定了先發消息才能收消息。所以,在實現群聊過程中,發現客戶端只有發了消息,才能接收到其他客戶端被轉發的消息。所以,為此我們進行了處理,就是讓客戶端多線程處理!即創建兩個線程,同時進行收發!
4.線程池訪問路由表的時候(就是底層的一個哈希),也是會涉及到線程安全的。但是,因為今天的實現并沒有規定消息的協議(是否退出、私法、群發、還是請求服務器處理…)。我們僅僅只是把收到的內容當字符串處理!
但是不管怎么說,線程池內每個線程訪問的時候,是會出現數據不一致的問題的!因為STL不是線程安全的,所以我們這里就粗暴一點,直接加鎖!
5.實踐的時候,因為我沒有多臺Linux機器,所以,使用了Windows進行輔助測試,測試是否能夠跨網通信!代碼如下:
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>#pragma warning(disable : 4996)#pragma comment(lib, "ws2_32.lib")std::string serverip = ""; // 填寫你的云服務器ip
uint16_t serverport = ; // 填寫你的云服務開放的端口號SOCKET sockfd;
struct sockaddr_in server;void recv_msg() {while (1) {char buffer[1024];struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}
}void send_msg() {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()); while (1) {std::string message; std::getline(std::cin, message);if (message.empty()) continue; sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr*)&server, sizeof(server));}
}int main() {WSADATA wsd; WSAStartup(MAKEWORD(2, 2), &wsd); sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == SOCKET_ERROR){std::cout << "socker error" << std::endl;return 1;}std::thread Recv(recv_msg);std::thread Send(send_msg);Recv.join();Send.join();closesocket(sockfd);WSACleanup();return 0;
}//int main()
//{
// WSADATA wsd;
// WSAStartup(MAKEWORD(2, 2), &wsd);
//
//
// 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());
//
// sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// if (sockfd == SOCKET_ERROR)
// {
// std::cout << "socker error" << std::endl;
// return 1;
// }
//
//
//
// std::string message;
// char buffer[1024];
// while (true)
// {
// std::cout << "please input: ";
// std::getline(std::cin, message);
// if (message.empty()) continue;
// sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr*)&server, sizeof(server));
// struct sockaddr_in temp;
// int len = sizeof(temp);
// int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
// if (s > 0)
// {
// buffer[s] = 0;
// std::cout << buffer << std::endl;
// }
// }
//
// closesocket(sockfd);
// WSACleanup();
// return 0;
//}
前面一份是用于版本3的測試,后面是用于版本1、2的測試!這里可以搜一下相關大模型了解一下用法,其實用法和Linux下的基本一致。