做app還要做網(wǎng)站么百度一下照片識別
目錄
??推薦
前言
六、Udp Server 端代碼
6.1 socket——創(chuàng)建套接字
6.2 bind——將套接字與一個 IP 和端口號進行綁定
6.3 recvfrom——從服務(wù)器的套接字里讀取數(shù)據(jù)
6.4 sendto——向指定套接字中發(fā)送數(shù)據(jù)
6.5 綁定 ip 和端口號時的注意事項
6.5.1 云服務(wù)器禁止直接綁定公網(wǎng) ip
6.5.2 綁定本地環(huán)回地址
6.5.2 端口號也不能胡亂綁定
6.6 服務(wù)端完整代碼
七、Udp Client 端代碼
八、基于 Udp 的指令處理
九、基于 Udp 的聊天室
9.1 server 端
9.1.1 地址轉(zhuǎn)換函數(shù)
9.2 client 端
??推薦
前些天發(fā)現(xiàn)了一個巨牛的人工智能學(xué)習(xí)網(wǎng)站,通俗易懂,風(fēng)趣幽默,忍不住分享一下給大家。點擊跳轉(zhuǎn)到網(wǎng)站
前言
本篇文章接上一篇?【Linux修行路】初識網(wǎng)絡(luò)套接字編程,所以目錄號從六開始。
六、Udp Server 端代碼
6.1 socket——創(chuàng)建套接字
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int socket(int domain, int type, int protocol);
-
domain:協(xié)議域(協(xié)議族)。該參數(shù)決定了?socket?的地址類型。在通信中必須采用對應(yīng)的地址,如?
AF_INET
?決定了要用?IPv4
?地址(32位的)與端口號(16位)的組合,AF_UNIX
?決定了要用一個絕對路徑名作為地址,AF_INET6
(IPv6)。 -
type:指定了 socket 的類型,如?
SOCK_STREAM
(流式套接字)、SOCK_DGRAM
(數(shù)據(jù)報式套接字)等等。 -
protocol:指定協(xié)議,如 IPPROTO_TCP(TCP傳輸協(xié)議)、PPTOTO_UDP(UDP 傳輸協(xié)議)、IPPROTO_SCTP(STCP 傳輸協(xié)議)、IPPROTO_TIPC(TIPC 傳輸協(xié)議)。
-
返回值:一個文件描述符,創(chuàng)建套接字的本質(zhì)其實就是打開一個文件。
void Init()
{// 1. 創(chuàng)建udp socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd_ < 0){lg(Fatal, "socket create error, errno: %d, error message: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "socket is created successful, sockfd: %d", sockfd_);
}
6.2 bind——將套接字與一個 IP 和端口號進行綁定
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
void Init()
{// 1. 創(chuàng)建 udp socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd_ < 0){lg(Fatal, "socket create error, errno: %d, error message: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "socket is created successful, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local)); // 將內(nèi)容清空成0local.sin_family = AF_INET; // 表明當(dāng)前結(jié)構(gòu)體的類型local.sin_port = htons(port_); // 當(dāng)前服務(wù)器的端口號local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 將字符串風(fēng)格的 ip 地址轉(zhuǎn)化成 4 字節(jié)的網(wǎng)絡(luò)序列// bind 的本質(zhì)就是把上面這個參數(shù)設(shè)置進內(nèi)核,設(shè)置到指定的套接字當(dāng)中int n = bind(sockfd_, (struct sockaddr*)&local, sizeof(local));if(n < 0) {lg(Fatal, "bind error, error: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success!");
}
因為是網(wǎng)絡(luò)通信,所以首先定義一個?struct sockaddr_in
?類型的對象,該對象中有四個字段,分別是:sin_family
(表明當(dāng)前結(jié)構(gòu)體的類型)、sin_port
(端口號)、sin_addr
(ip地址)、sin_zero
(填充字段)。我們需要自己設(shè)置前三個字段的信息。其中?sin_family
?的值和?socket
?函數(shù)中的?domain
?參數(shù)保持一致;其次,端口號和 ip 地址,是需要再網(wǎng)絡(luò)中傳輸?shù)?#xff0c;因此要把主機序列轉(zhuǎn)換成網(wǎng)絡(luò)序列。其中使用?htons
?將端口號從主機轉(zhuǎn)成網(wǎng)絡(luò)序列。ip 地址實際上是一個 4 字節(jié)的?uint32_t
?類型,但是為了配合用戶的使用習(xí)慣,我們讓用戶輸入的 ip 地址是一個?string
?類型,例如 “xxx.xxx.xxx.xxx” 的點分形式,因此 ip 地址的轉(zhuǎn)換有兩步,分別是將string
?類型轉(zhuǎn)換成?uint32_t
?類型,然后再從主機轉(zhuǎn)化成網(wǎng)絡(luò)序列。這兩個轉(zhuǎn)化使用?inet_addr
?接口就可以實現(xiàn),其次?sin_addr
?的類型是?struct in_addr
,該結(jié)構(gòu)體中就只有一個字段?in_addr_t s_addr;
?其中?in_addr_t
?就是?uint32_t
。
bind
?本質(zhì)就是將我們上一步中創(chuàng)建的套接字與一個 ip 和端口號建立關(guān)聯(lián)。
6.3 recvfrom——從服務(wù)器的套接字里讀取數(shù)據(jù)
#include <sys/types.h>
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
-
buf:接收緩沖區(qū)。
-
len:接收緩沖區(qū)的大小。
-
flags:默認(rèn)設(shè)置為 0,表示阻塞。
-
src_addr:輸出型參數(shù),獲取客戶端的套接字信息,也就是獲取客戶端的 ip 和端口號信息。因為是?
udp
?網(wǎng)絡(luò)通信,所以這里傳入的還是?struct sockaddr_in
?類型的對象地址。 -
addrlen:這里就是?
struct sockaddr_in
?對象的大小。 -
返回值:成功會返回獲取到數(shù)據(jù)的字節(jié)數(shù);失敗返回 -1。
void Run()
{isrunning_ = true;while (isrunning_){char buffer[size];struct sockaddr_in client;socklen_t len = sizeof(client);// 從當(dāng)前服務(wù)器的套接字中讀取數(shù)據(jù)int ret = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);if (ret < 0){lg(Warning, "recvfrom error, error: %d, err string: %s", errno, strerror(errno));continue;}buffer[ret] = 0;std::string info = buffer;std::string echo_string = "server echo# <b>" + info;}
}
小Tips:我們發(fā)送和接收的數(shù)據(jù)內(nèi)容是不需要我們自己進行主機專網(wǎng)絡(luò),再從網(wǎng)絡(luò)轉(zhuǎn)主機的,數(shù)據(jù)內(nèi)容會由?recvfrom
?函數(shù)和?sendto
?函數(shù)自動幫我們進行轉(zhuǎn)換。
6.4 sendto——向指定套接字中發(fā)送數(shù)據(jù)
#include <sys/types.h>
#include <sys/socket.h>ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
-
sockfd:當(dāng)前服務(wù)器的套接字,發(fā)送網(wǎng)絡(luò)數(shù)據(jù)本質(zhì)就是先向該主機的網(wǎng)卡(本質(zhì)上就是文件)中進行寫入。
-
buf:待發(fā)送的數(shù)據(jù)緩沖區(qū)。
-
len:數(shù)據(jù)緩沖區(qū)的大小。
-
flags:默認(rèn)設(shè)置為 0。
-
dest_addr:接收方的套接字信息,這里也就是客戶端的套接字信息。
-
addrlen:?
struct sockaddr_in
?對象的大小。
void Run()
{isrunning_ = true;while (isrunning_){// 從套接字中讀取數(shù)據(jù)char buffer[num];struct sockaddr_in client;socklen_t len = sizeof(client);int ret = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);if (ret < 0){lg(Warning, "recvfrom error, error: %d, err string: %s", errno, strerror(errno));continue;}buffer[ret] = 0;std::string info = buffer;std::string echo_string = "server echo# <b>" + info;// 向 client端 發(fā)送數(shù)據(jù)int n = sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);if(n < 0){lg(Warning, "sendto error, error: %d, err string: %s", errno, strerror(errno));continue; }}
}
到這里,服務(wù)端的代碼就編寫的差不多了,接下來可以使用?netstat -naup
?指令看看效果,其中?net
?就表示網(wǎng)絡(luò),stat
?就表示狀態(tài),n
?選項是把所有能顯示成數(shù)字的信息都以數(shù)字的形式進行顯示;a
表示所有;u
?表示 udp;p
?表示顯示 PID 信息。
6.5 綁定 ip 和端口號時的注意事項
6.5.1 云服務(wù)器禁止直接綁定公網(wǎng) ip
一般建議服務(wù)器端的代碼 bind 時綁定 ip 0.0.0.0,表示任意地址綁定,這樣只要是發(fā)送到這臺主機的數(shù)據(jù),該服務(wù)器進程都能收到,然后根據(jù)端口號向上交付。因為一個服務(wù)器可能有多個 ip,如果服務(wù)端的進程只 bind 某一個固定的 ip,那么通過其它 ip 發(fā)送到該服務(wù)器的數(shù)據(jù),這個進程就無法收到。local.sin_addr.s_addr = INADDR_ANY
。
6.5.2 綁定本地環(huán)回地址
任何服務(wù)器進程都可以綁定?127.0.0.1
?這個 ip 地址,這個 ip 地址叫做本地環(huán)回地址,綁定了這個地址后,該進程不會向網(wǎng)絡(luò)中發(fā)送數(shù)據(jù),但是還是會走網(wǎng)絡(luò)協(xié)議棧,通常用來進行 CS 的測試。
6.5.2 端口號也不能胡亂綁定
#include "UdpServer.hpp"
#include <memory>int main()
{std::unique_ptr<UdpServer> svr(new UdpServer(1023));svr->Init();svr->Run();return 0;
}
一般 [0, 1023] 是系統(tǒng)內(nèi)定的端口號,都要有固定的應(yīng)用層協(xié)議使用,例如:http
(80)、https
(443),端口號的范圍是 0~65535,建議我們平時在自己的代碼中就往大了去綁。
6.6 服務(wù)端完整代碼
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include "log.hpp"
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <functional>#define num 1024extern Log lg;enum
{SOCKET_ERR = 1,BIND_ERR
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";using func_t = std::function<std::string(const std::string &)>;class UdpServer
{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip): ip_(ip), port_(port), isrunning_(false){}void Init(){// 1. 創(chuàng)建 udp socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){lg(Fatal, "socket create error, errno: %d, error message: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "socket is created successful, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local)); // 將內(nèi)容清空成0local.sin_family = AF_INET; // 表明當(dāng)前結(jié)構(gòu)體的類型local.sin_port = htons(port_); // 當(dāng)前服務(wù)器的端口號local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 將字符串風(fēng)格的 ip 地址轉(zhuǎn)化成 4 字節(jié)的網(wǎng)絡(luò)序列// bind 的本質(zhì)就是把上面這個參數(shù)設(shè)置進內(nèi)核,設(shè)置到指定的套接字當(dāng)中int n = bind(sockfd_, (struct sockaddr *)&local, sizeof(local));if (n < 0){lg(Fatal, "bind error, error: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success!");}void Run(func_t func) // 參數(shù)是數(shù)據(jù)處理函數(shù){isrunning_ = true;while (isrunning_){// 從套接字中讀取數(shù)據(jù)char buffer[num];struct sockaddr_in client;socklen_t len = sizeof(client);int ret = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);if (ret < 0){lg(Warning, "recvfrom error, error: %d, err string: %s", errno, strerror(errno));continue;}printf("client say@ %s\n", buffer);buffer[ret] = 0;std::string info = buffer;std::string echo_string = func(info); // 數(shù)據(jù)處理// 向 client端 發(fā)送數(shù)據(jù)int n = sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);if(n < 0){lg(Warning, "sendto error, error: %d, err string: %s", errno, strerror(errno));continue; }}}~UdpServer(){close(sockfd_);}private:int sockfd_; // 網(wǎng)絡(luò)文件描述符std::string ip_; // 主機的 ip 地址uint16_t port_; // 服務(wù)器的端口號bool isrunning_; // 是否在運行
};
#include "UdpServer.hpp"
#include <memory>
#include <string>void Usage(const char *command)
{std::cout << "\n\tUsage: " << command << " port[1024+]" << std::endl;
}std::string Hander(const std::string &str)
{std::string res = "Server get a message: " + str;return res;
}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->Run(Hander);return 0;
}
七、Udp Client 端代碼
因為一個端口號只能被一個進程 bind,客戶端的應(yīng)用是非常多的,如果在客戶端采用靜態(tài) bind,那可能會出現(xiàn)兩個應(yīng)用同時 bind 同一個端口號,此時就注定了這兩個應(yīng)用一定是不能同時運行的。為了解決這個問題,一般不建議客戶端 bind 一個固定的端口,而是由操作系統(tǒng)來進行動態(tài)的 bind,這樣就可以避免端口號發(fā)生沖突。這也間接說明,對一個 client 端的進程來說,它的端口號是幾并不重要,只要能夠標(biāo)識該進程在主機上的唯一性就可以。因為,一般都是由 clinet 端主動的向?server?端發(fā)送消息,所以 client 一定是能夠知道 client 端的端口號。相反,服務(wù)器的端口號必須是確定的。因此,在編寫客戶端的代碼時,第一步就是創(chuàng)建套接字,創(chuàng)建完無需進行 bind,直接向服務(wù)器發(fā)送數(shù)據(jù),發(fā)送的時候,操作系統(tǒng)會為我們進行動態(tài) bind。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <memory>
#include <string>
#include <cstring>
#include <arpa/inet.h>using namespace std;void Usage(const char *command)
{std::cout << "\n\tUsage: " << command << " serverip serverport" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);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);// 1. 創(chuàng)建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socker error" << endl;exit(1);}// 2. 接下來直接發(fā)送數(shù)據(jù)string message;char buffer[1024];while (true){cout << "Please Enter@ ";getline(cin, message);// 發(fā)送數(shù)據(jù)sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);struct sockaddr_in temp;socklen_t temp_len = sizeof(temp);// 接收服務(wù)器數(shù)據(jù)ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &temp_len);if(s > 0){buffer[s] = 0;cout << buffer << endl;}}close(sockfd);return 0;
}
八、基于 Udp 的指令處理
服務(wù)端在收到用戶端的數(shù)據(jù)后,會對數(shù)據(jù)進行加工處理,然后再返回給客戶端,可以將數(shù)據(jù)處理的過程獨立出來,將數(shù)據(jù)處理的函數(shù)作為參數(shù)傳遞給服務(wù)端的?Run
?方法,當(dāng)前這個場景就是基于 Udp 的,客戶端輸入指令,客戶端執(zhí)行對應(yīng)的指令,并將執(zhí)行結(jié)果返回給用戶。這里服務(wù)端指令的執(zhí)行通過調(diào)用?popen
?函數(shù)即可。
#include <stdio.h>FILE *popen(const char *command, const char *type);int pclose(FILE *stream);
popen
?函數(shù)會調(diào)用?fork
?創(chuàng)建子進程,然后子進程執(zhí)行程序替換,去執(zhí)行指令。其中?command
?參數(shù)就是要執(zhí)行的指令,type
?使用?"r"
?表示讀取,使用?"w"
?表示寫入,根據(jù)這個參數(shù),popen
?會建立管道連接到子進程的標(biāo)準(zhǔn)輸入輸出設(shè)備或標(biāo)準(zhǔn)輸入設(shè)備,然后返回一個文件指針,隨后進程便可利用此文件指針來讀取子進程的輸出設(shè)備或者寫入到子進程的標(biāo)準(zhǔn)輸入設(shè)備中。此外,所有使用文件指針 (FILE*
) 操作的函數(shù)也都可以使用,出了?fclose
?以外。返回值:如果床工則返回文件指針,否則返回?NULL
,錯誤原因存于?errno
?中。
客戶端的主題代碼不動,只用將?ExcuteCommand
?函數(shù)作為?svr->Run();
?的參數(shù)傳遞進去即可。
bool SafeCheak(const std::string &cmd)
{std::vector<std::string> key_word = {"rm", "mv", "cp", "kill", "sudo", "unlink", "uninstall", "yum", "top"};for(auto & key : key_word){if(cmd.find(key) != std::string::npos){return false;}}return true;
} std::string ExcuteCommand(const std::string &cmd)
{std::cout << "get a message, cmd: %s" << std::endl;// 指令檢查if(!SafeCheak(cmd)) return "This action is prohibited";FILE *fp = popen(cmd.c_str(), "r");if(fp == NULL){perror("popen");return "error";}// 讀取執(zhí)行結(jié)果std::string result;char buffer[4096];while(true){char *ret = fgets(buffer, sizeof(buffer), fp);if(ret == NULL) break;result += buffer;}fclose(fp);return result;
}
九、基于 Udp 的聊天室
9.1 server 端
因為是聊天室,只要是進入該聊天室的用戶,在該聊天室中發(fā)送的消息,應(yīng)該是可以被所有在線用戶看到的,所以我們需要在客戶端維護一張在線用戶列表,采用?unordered_map
?結(jié)構(gòu),讓用戶的?ip
?作為?key
?值,用戶端的套接字作為?value
,當(dāng)一個客戶進入該聊天室的時候,客戶端應(yīng)該進行檢查,看其是否在在線列表中,如果不在,應(yīng)該先將其加入到在線列表。然后客戶端需要將該用戶發(fā)送的信息,再轉(zhuǎn)發(fā)給其他所有在線用戶,因此需要去遍歷在線用戶列表進行?sendto
。服務(wù)端在轉(zhuǎn)發(fā)用戶消息前,先進行輕加工,在消息前加上用戶端的 ip 和端口,用來標(biāo)識該信息是誰發(fā)送的。因此服務(wù)端在接收到用戶端信息后,需要對從網(wǎng)絡(luò)中收到的用戶套接字信息進行網(wǎng)路轉(zhuǎn)主機操作。對于端口號,可以采用?ntohs
?接口將網(wǎng)絡(luò)序列轉(zhuǎn)換成主句序列,對于 ip,可以采用?inet_ntoa
?將 4 字節(jié)的網(wǎng)絡(luò) ip 序列轉(zhuǎn)化成字符串類型的主機序列。
// UdpServer.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include "log.hpp"
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <functional>
#include <unordered_map>#define num 1024extern Log lg;enum
{SOCKET_ERR = 1,BIND_ERR
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";using func_t = std::function<std::string(const std::string &, const std::string &, const uint16_t &)>;class UdpServer
{
private:void CheckUser(const sockaddr_in &client, const std::string &client_ip, uint16_t client_port){auto pos = userinfo_.find(client_ip);if (pos == userinfo_.end()){userinfo_.insert({client_ip, client});printf("[%s-%d] Enter the chat room\n", client_ip.c_str(), client_port);}}void Broadcast(const std::string &info, const std::string &client_ip, uint16_t client_port){for(auto &user : userinfo_){std::string message = "[";message += client_ip;message += "-";message += std::to_string(client_port);message += "]# ";message += info;socklen_t len = sizeof(user.second);sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr *)&(user.second), sizeof(user.second));}}public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip): ip_(ip), port_(port), isrunning_(false){}void Init(){// 1. 創(chuàng)建 udp socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){lg(Fatal, "socket create error, errno: %d, error message: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "socket is created successful, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local)); // 將內(nèi)容清空成0local.sin_family = AF_INET; // 表明當(dāng)前結(jié)構(gòu)體的類型local.sin_port = htons(port_); // 當(dāng)前服務(wù)器的端口號local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 將字符串風(fēng)格的 ip 地址轉(zhuǎn)化成 4 字節(jié)的網(wǎng)絡(luò)序列// bind 的本質(zhì)就是把上面這個參數(shù)設(shè)置進內(nèi)核,設(shè)置到指定的套接字當(dāng)中int n = bind(sockfd_, (struct sockaddr *)&local, sizeof(local));if (n < 0){lg(Fatal, "bind error, error: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success!");}// 數(shù)據(jù)傳輸——(獲取和發(fā)送)void Run(){isrunning_ = true;while (isrunning_){// 從套接字中讀取數(shù)據(jù)char buffer[num];struct sockaddr_in client;socklen_t len = sizeof(client);int ret = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);if (ret < 0){lg(Warning, "recvfrom error, error: %d, err string: %s", errno, strerror(errno));continue;}buffer[ret] = 0;uint16_t client_port = ntohs(client.sin_port); // 獲取client 端的端口號std::string client_ip = inet_ntoa(client.sin_addr); // 獲取 client 端的 ip 地址CheckUser(client, client_ip, client_port);std::string info = buffer;Broadcast(info, client_ip, client_port); // 將收到的消息廣播給所有的在線用戶// std::string info = buffer;// std::string echo_string; // = func(info, client_ip, client_port);// // 向 client端 發(fā)送數(shù)據(jù)// int n = sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&client, len);// if (n < 0)// {// lg(Warning, "sendto error, error: %d, err string: %s", errno, strerror(errno));// continue;// }}}~UdpServer(){close(sockfd_);}private:int sockfd_; // 網(wǎng)絡(luò)文件描述符std::string ip_; // 主機的 ip 地址uint16_t port_; // 服務(wù)器的端口號bool isrunning_; // 是否在運行std::unordered_map<std::string, sockaddr_in> userinfo_; // 維護一張用戶信息表。
};
// main.cc
#include "UdpServer.hpp"
#include <memory>
#include <string>
#include <vector>
#include <iostream>void Usage(const char *command)
{std::cout << "\n\tUsage: " << command << " port[1024+]" << std::endl;
}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->Run();return 0;
}
補充:upd
?的?socket
?是全雙工的,可以同時讀數(shù)據(jù)和發(fā)數(shù)據(jù)。
9.1.1 地址轉(zhuǎn)換函數(shù)
將字符串轉(zhuǎn)化成 4 字節(jié)網(wǎng)絡(luò)序列:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
int inet_pton(int af, const char *src, void *dst);
網(wǎng)絡(luò)序列轉(zhuǎn)化成字符串:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
inet_ntoa
?函數(shù)的返回值是一個地址,那轉(zhuǎn)化出來的字符串在哪呢?答案是,在?inet_ntoa
?函數(shù)的內(nèi)部為我們申請了一塊內(nèi)存來保存?ip
?字符串的結(jié)果,main
?手冊上說,inet_ntoa
?函數(shù),是把這個返回結(jié)果放到了靜態(tài)存儲區(qū),這個時候不需要我們手動釋放。但這也導(dǎo)致在一個進程中第二次調(diào)用?inet_ntoa
?函數(shù)時,會對上一次的轉(zhuǎn)換結(jié)果進行覆蓋。在?APUE
?中明確的提出?inet_ntoa
?不是線程安全的函數(shù),所以在多線程編程下建議使用?inet_ntop
?函數(shù),這個函數(shù)由調(diào)用者自己提供一個緩沖區(qū)保存結(jié)果,可以規(guī)避線程安全問題。
9.2 client 端
client
?端主要有兩個功能,一是該用戶發(fā)送數(shù)據(jù),二是該用戶獲取其他用戶發(fā)送的消息,本質(zhì)是獲取服務(wù)端的消息,因為其他用戶的消息是先交給服務(wù)端的。因此這里需要兩個線程,一個線程用來獲取用戶輸入,將用戶的輸入消息發(fā)送給服務(wù)器,另一個線程用來接收服務(wù)器的消息,采用兩個線程的本質(zhì)原因是,在獲取用戶輸入的時候,如果用戶一直沒有進行輸入,那么會阻塞住,就收不到服務(wù)器的消息,所以這里采用兩個線程將獲取用戶輸入和獲取服務(wù)端的消息分開。這兩個線程一個線程往套接字里面進行寫入,一個從套接字里面進行讀取,看似在訪問同一份資源,但實際上,Udp 套接字是一個全雙工的,發(fā)數(shù)據(jù)和收數(shù)據(jù)都有自己獨立的緩沖區(qū),因此不存在線程安全。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <memory>
#include <string>
#include <cstring>
#include <arpa/inet.h>using namespace std;struct ThreadData
{int socckfd;sockaddr_in server;
};void Usage(const char *command)
{std::cout << "\n\tUsage: " << command << " serverip serverport" << std::endl;
}void *send_rountine(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);string message;while(true){cout << "Please Enter@ ";getline(cin, message);// 發(fā)送數(shù)據(jù)sendto(td->socckfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), sizeof(td->server));}
}void *recv_rountine(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);char buffer[1024];while(true){struct sockaddr_in temp;socklen_t temp_len = sizeof(temp);// 接收服務(wù)器數(shù)據(jù)ssize_t s = recvfrom(td->socckfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &temp_len);if(s > 0){buffer[s] = 0;cerr << buffer << endl;}}
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 1. 創(chuàng)建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socker error" << endl;exit(1);}// 創(chuàng)建倆線程,一個獲取用戶輸入向 server 發(fā)送,一個接收 server 端消息pthread_t send, recv;ThreadData *threaddata = new ThreadData();threaddata->socckfd = sockfd;threaddata->server.sin_family = AF_INET;threaddata->server.sin_port = htons(serverport);threaddata->server.sin_addr.s_addr = inet_addr(serverip.c_str()); pthread_create(&send, nullptr, send_rountine, threaddata);pthread_create(&recv, nullptr, recv_rountine, threaddata);pthread_join(send, nullptr);pthread_join(recv, nullptr);close(sockfd);return 0;
}
這里有個小細(xì)節(jié),就是將用戶輸入消息的終端和顯示消息的終端分離,在打印消息時,采用?cerr
,然后在啟動?client
?的時候,將標(biāo)準(zhǔn)錯誤重定向到另一個終端。
🎁結(jié)語:
????????今天的分享到這里就結(jié)束啦!如果覺得文章還不錯的話,可以三連支持一下,您的支持就是我前進的動力!