Linux手把手教你實(shí)現(xiàn)udp服務(wù)器的詳細(xì)過程
前言
上一篇文章中我們講到了很多的網(wǎng)絡(luò)名詞以及相關(guān)知識,下面我們就直接進(jìn)入udp服務(wù)器的實(shí)現(xiàn)。
一、udp服務(wù)器的實(shí)現(xiàn)
首先我們需要?jiǎng)?chuàng)建五個(gè)文件(文件名可以自己命名也可以和我一樣),分別是makefile,udpclient.cc,udpclient.hpp,udpserver.cc,udpserver.hpp,下面我們先進(jìn)行makefile的編寫,在makefile中我們要一次創(chuàng)建兩個(gè)可執(zhí)行程序:
cc=g++ .PHONY:all all:udpClient udpServer udpClient:udpClient.cc $(cc) -o $@ $^ -std=c++11 udpServer:udpServer.cc $(cc) -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f udpClient udpServer
我們通過all就可以創(chuàng)建多個(gè)可執(zhí)行程序了,對于cc這個(gè)變量我們設(shè)置為g++,以后如果想換其他的編譯器就可以直接替換了。
在udpserver.hpp這個(gè)文件中我們先寫出整體框架:
namespace Server { class udpServer { public: udpServer() { } void InitServer() { } void start() { } ~udpServer() { } private: //服務(wù)器一定要有自己的服務(wù)端口號(注意端口號是16位的) uint16_t _port; //端口號 //實(shí)際上一款服務(wù)器不建議指明一個(gè)IP string _ip; //ip }; }
那么我們現(xiàn)在服務(wù)器的ip填多少呢?實(shí)際上我們只是完成測試,所以ip就填0.0.0.0就好了,這樣的話任意的ip都能訪問我們的服務(wù)器,所以我們定義一個(gè)static變量來保存ip:
static const string defaultIp = "0.0.0.0";
有了ip和端口號后,我們就可以用構(gòu)造函數(shù)初始化了:
udpServer(const uint16_t& port,const string ip = defaultIp) :_port(port) ,_ip(ip) { }
我們的服務(wù)器未來要啟動(dòng)的話就必須先初始化然后再啟動(dòng),所以我們寫了init和start接口,那么該如何初始化呢?實(shí)際上不管是udp還是tcp,我們初始化都是需要套接字的,下面我們看看套接字的接口:
如何理解套接字呢,我們都知道linux一切皆文件,所以未來的網(wǎng)絡(luò)通信一定是在同一個(gè)文件中只要和網(wǎng)卡設(shè)備關(guān)聯(lián)起來就實(shí)現(xiàn)了網(wǎng)絡(luò)通信,所以套接字的目的實(shí)際上是創(chuàng)建一個(gè)文件,可以看到我們的套接字有三個(gè)參數(shù),第一個(gè)參數(shù)的解釋是域,實(shí)際上就是讓我們選擇是進(jìn)行網(wǎng)絡(luò)通信還是本地通信,這里我們一般選擇AF_INET選項(xiàng),代表使用IPV4協(xié)議的網(wǎng)絡(luò)通信。第二個(gè)參數(shù)是type,表面套接字要向我們提供服務(wù)的類型,怎么理解呢,如下圖:
我們現(xiàn)在所寫的UDP服務(wù)器的特點(diǎn)是不可靠傳輸無連接,而這正是與SOCK_DGRAM這個(gè)選項(xiàng)所匹配的,我們查看這個(gè)選項(xiàng)的解釋可以看到:DGRAM適用于不可靠傳輸,連接少
我們下一篇要實(shí)現(xiàn)的TCP服務(wù)器,就會(huì)用到SOCK_STREAM這個(gè)選項(xiàng),因?yàn)檫@個(gè)選項(xiàng)的解釋是面向流式服務(wù),而我們TCP的特點(diǎn)就是面向字節(jié)流。
第三個(gè)參數(shù)我們一般缺省為0,因?yàn)檫@個(gè)參數(shù)代表我們未來要采用什么協(xié)議,如果我們寫為0,那么這個(gè)接口會(huì)根據(jù)我們填的前兩個(gè)參數(shù)來幫我們確定第三個(gè)參數(shù)是選擇TCP協(xié)議還是UDP協(xié)議。
這個(gè)接口的返回值相信大家也看到了,沒錯(cuò)!一旦創(chuàng)建套接字成功,那么就會(huì)給我們返回一個(gè)文件描述符,如果失敗則會(huì)給我們返回-1并且提供錯(cuò)誤碼。
了解了socket這個(gè)接口,那么我們下一步就是增加一個(gè)私有變量來接收socket返回的文件描述符(注意:這個(gè)文件描述符會(huì)被后面的接口多次用到):
然后我們在構(gòu)造函數(shù)中將這個(gè)文件描述符初始化為-1:
udpServer(const uint16_t& port,const string ip = defaultIp) :_port(port) ,_ip(ip) ,_sockfd(-1) { }
然后我們初始化第一步:使用套接字
void InitServer() { //UDP第一步:創(chuàng)建了一個(gè)套接字 _sockfd = socket(AF_INET,SOCK_DGRAM,0); if (_sockfd==-1) { cerr<<"socket error: "<<errno<<" : "<<strerror(errno)<<endl; exit(SOCKET_ERROR); } cout<<"server socket success: "<<" : "<<_sockfd<<endl; }
如果套接字創(chuàng)建失敗,就算沒有給我們的文件描述符返回-1,由于我們初始化的時(shí)候就初始化為-1,所以還是會(huì)報(bào)錯(cuò),注意:一旦連套接字都沒創(chuàng)建成功,那么就沒有繼續(xù)的必要了直接退出即可,這里我們直接用枚舉列出所有的退出碼然后在退出的時(shí)候使用:
enum { SOCKET_ERROR = 2 };
創(chuàng)建成功我們就直接打印一下文件描述符即可。
下面進(jìn)入初始化第二步:綁定端口和ip
首先第一個(gè)參數(shù)就是我們使用socket接口給我們返回的文件描述符,第二個(gè)參數(shù)是什么呢?大家看到這個(gè)參數(shù)名struct sockaddr*是否感到熟悉呢?沒錯(cuò)就是我們上一篇講到的sockaddr結(jié)構(gòu):
注意我們用的IPV4協(xié)議要用sockaddr_in這個(gè)結(jié)構(gòu),但是接口參數(shù)是sockaddr*這個(gè)結(jié)構(gòu),所以我們用的時(shí)候要做一下強(qiáng)制類型轉(zhuǎn)換??梢钥吹轿覀兊倪@個(gè)結(jié)構(gòu)有4個(gè)位置需要我們填充,第一個(gè)AF_INET代表協(xié)議家族,第二個(gè)是端口號,第三個(gè)是IP地址,第四個(gè)是這個(gè)結(jié)構(gòu)體的大小。
第三個(gè)參數(shù)是這個(gè)結(jié)構(gòu)體的長度。
對于bind這個(gè)接口,如果成功則返回0,如果失敗則返回-1.
由于bind的第二個(gè)參數(shù)是結(jié)構(gòu)體指針,所以我們需要先創(chuàng)建一個(gè)新的結(jié)構(gòu)體,然后對這個(gè)結(jié)構(gòu)體進(jìn)行填充,填充后傳入?yún)?shù):
struct sockaddr_in local; //在棧(用戶)上定義了一個(gè)結(jié)構(gòu)體變量 bzero(&local,sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); //給別人發(fā)消息要將port和ip發(fā)送給對方 htons主機(jī)轉(zhuǎn)網(wǎng)絡(luò)序列(port是short類型) local.sin_addr.s_addr = inet_addr(_ip.c_str()); //1.string->uint32_t 2.主機(jī)轉(zhuǎn)網(wǎng)絡(luò),ip是四字節(jié)htonl
bzero這個(gè)接口可以將我們的結(jié)構(gòu)體里面的內(nèi)容初始化為0,然后我們進(jìn)行填充首先協(xié)議家族填寫AF_INET這里是固定寫法,然后就是填寫端口號和ip地址,對于端口號,在結(jié)構(gòu)體中的類型是16字節(jié)的short短整型,而htons這個(gè)接口可以將主機(jī)字節(jié)序轉(zhuǎn)化為網(wǎng)絡(luò)字節(jié)序(還記得我們上一篇講的內(nèi)容嗎?網(wǎng)絡(luò)中所有字節(jié)序必須是大端存儲(chǔ),而主機(jī)中有可能大端有可能小端,所以hton這個(gè)接口就是將任意的主機(jī)字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序的接口),htons后面的s代表要轉(zhuǎn)化為16字節(jié)的,如果你的port是32字節(jié)的,那么你就需要用htonl轉(zhuǎn)換為long類型。
對于ip的填充,首先結(jié)構(gòu)體中的ip的類型是32位的,而我們剛剛在類內(nèi)定義的是一個(gè)字符串,所以我們需要先將字符串轉(zhuǎn)換為32位整形,然后再將這個(gè)32位整形由主機(jī)字節(jié)序轉(zhuǎn)化為網(wǎng)絡(luò)字節(jié)序,所以正常的步驟是:1.string->uint32_t 2.htonl(uint32_t) 但是現(xiàn)在我們有一個(gè)很好用的接口,這個(gè)接口是inet_addr,下面我們看看這個(gè)接口:
我們可以看到inet_addr的參數(shù)是一個(gè)const char*類型,這是什么呢?實(shí)際上這個(gè)類型就是我們ip常用的點(diǎn)分十進(jìn)制類型,這個(gè)函數(shù)的返回值是in_addr_t,也就是說這個(gè)函數(shù)可以直接將點(diǎn)分十進(jìn)制類型轉(zhuǎn)化為我們結(jié)構(gòu)體中所需要的ip類型。
我們將這個(gè)結(jié)構(gòu)體填充完畢后,下面就直接綁定端口號和ip:
int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local)); if (n==-1) { cerr<<"bind error: "<<errno<<" : "<<strerror(errno)<<endl; exit(BIND_ERR); }
前面我們說過,bind的參數(shù)與我們ipv4協(xié)議使用的結(jié)構(gòu)體類型不一樣需要強(qiáng)制轉(zhuǎn)化。當(dāng)我們綁定失敗,我們就打印錯(cuò)誤信息,然后加一個(gè)bind接口的錯(cuò)誤碼用于返回:
enum { SOCKET_ERROR = 2 ,BIND_ERR };
綁定結(jié)束后我們的服務(wù)器初始化接口就結(jié)束了,下面我們進(jìn)入服務(wù)器啟動(dòng)的接口,在這里我們要注意,服務(wù)器啟動(dòng)的本質(zhì)就是一個(gè)死循環(huán),就比如我們的手機(jī)系統(tǒng),如果不是主動(dòng)的退出,我們的手機(jī)是不會(huì)關(guān)機(jī)的。
對于udp服務(wù)器的啟動(dòng),我們先大概的思考一下:./udpserver ip port也就是說需要三個(gè)參數(shù),所以我們可以先設(shè)計(jì)一下udpserver.cc:
首先對于不懂如何啟動(dòng)服務(wù)器的用戶我們需要加一個(gè)使用手冊,保證用戶可以正常啟動(dòng)服務(wù)器:
static void Usage(string proc) { cout<<"Usage:\n\t"<<proc<<" local_ip local_port\n\n"; } int main(int argc,char* argv[]) { if (argc!=3) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[2]); string ip = argv[1]; unique_ptr<udpServer> usvr(new udpServer(port,ip)); usvr->InitServer(); usvr->start(); return 0; }
對于main函數(shù)的參數(shù)我們之前已經(jīng)講過,argc代表你傳了幾個(gè)參數(shù),argv這個(gè)數(shù)組對應(yīng)的下標(biāo)就是我們的參數(shù)。我們的目的是:./udpserver ip port這樣使用,所以一共有三個(gè)參數(shù),如果用戶沒有傳3個(gè)參數(shù),那么我們就直接提示如何使用并且退出程序,這里我們也可以弄一個(gè)錯(cuò)誤碼寫到枚舉中:
enum { USAGE_ERR = 1 ,SOCKET_ERROR ,BIND_ERR };
如果用戶輸入成功,那么我們先獲取用戶輸入的端口號,因?yàn)橛脩糨斎氲氖亲址?,所以需要將字符串轉(zhuǎn)化為整形,我們用uint16_t的類型來接收端口號,因?yàn)槲覀兊膕erver類中的ip是string的,所以可以直接用string變量獲取ip地址。然后我們用一個(gè)智能指針來管理服務(wù)器,在服務(wù)器中使用端口號和ip構(gòu)造服務(wù)器,然后對服務(wù)器進(jìn)行初始化和啟動(dòng)即可。
下面我們講解一個(gè)在綁定前填充結(jié)構(gòu)體中ip地址的問題:實(shí)際上我們在正在做項(xiàng)目的時(shí)候,是不會(huì)直接像下面這樣指明一個(gè)IP的:
真實(shí)的寫法應(yīng)該是下面這樣:
local.sin_addr.s_addr = INADDR_ANY; //任意地址綁定才是服務(wù)器的真實(shí)寫法
什么意思呢?實(shí)際上就是當(dāng)我們將服務(wù)器的IP設(shè)為ANY(本質(zhì)其實(shí)是0),就代表未來發(fā)給我的數(shù)據(jù)只要是綁定了我的端口那么就能與我通信,這樣就不會(huì)漏掉沒有我IP地址的服務(wù)器給我發(fā)的消息了。還記得我們剛開始寫的IP是什么嗎?沒錯(cuò)就是全0,也就是說我們現(xiàn)在寫的這個(gè)服務(wù)器是不需要我們具體的IP只需要通過端口號就可以啟動(dòng)臺(tái)服務(wù)器,并且未來客戶端訪問我們的服務(wù)器的時(shí)候是不需要指明IP的,任意一個(gè)IP+特定的端口號都能訪問我們這臺(tái)服務(wù)器。既然不需要IP,下面我們就修改一下代碼:
static void Usage(string proc) { cout<<"Usage:\n\t"<<proc<<" local_port\n\n"; } int main(int argc,char* argv[]) { if (argc!=2) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); unique_ptr<udpServer> usvr(new udpServer(handerMessage,port)); usvr->InitServer(); usvr->start(); return 0; }
所以實(shí)際上一個(gè)服務(wù)器的IP不重要,只要我們有端口號就能啟動(dòng)這臺(tái)服務(wù)器,并且客戶端用任意的IP和我們服務(wù)器特定的端口號就可以和我們的服務(wù)器通信。
下面我們編寫start接口的代碼,一旦啟動(dòng)我們就要接受數(shù)據(jù),所以我們先認(rèn)識一個(gè)接口:
這個(gè)接口的第一個(gè)參數(shù)是我們創(chuàng)建套接字返回的文件描述符,意思就是我們從哪個(gè)套接字里讀數(shù)據(jù)。第二個(gè)參數(shù)是一個(gè)緩沖區(qū),第三個(gè)參數(shù)是這個(gè)緩沖區(qū)的長度,2和3這兩個(gè)參數(shù)代表的是你讀到的數(shù)據(jù)要放在哪個(gè)緩沖區(qū)里,第四個(gè)參數(shù)是讀取方式,這里我們默認(rèn)填0代表阻塞式讀取,也就是說客戶端不給我們服務(wù)端發(fā)消息時(shí),我們就一直等待客戶端發(fā)消息,這就叫阻塞式讀取。第五個(gè)參數(shù)和第六個(gè)參數(shù)非常重要,這兩個(gè)參數(shù)是輸出型參數(shù),也就是說未來客戶端給我們發(fā)消息時(shí),會(huì)將數(shù)據(jù)放到緩沖區(qū)中,然后會(huì)將客戶端的端口號和IP放到struct sockaddr*這個(gè)結(jié)構(gòu)體當(dāng)中,第六個(gè)參數(shù)就是這個(gè)結(jié)構(gòu)體的長度,我們可以理解為:我們只需要?jiǎng)?chuàng)建一個(gè)空的結(jié)構(gòu)體,然后客戶端發(fā)消息后這個(gè)接口就會(huì)將客戶端的端口號和IP放到我們自己創(chuàng)建的結(jié)構(gòu)體中。
對于這個(gè)接口的返回值,如果成功則會(huì)給我們返回讀到數(shù)據(jù)的字節(jié)數(shù),如果失敗返回-1.
static const int gnum = 1024; void start() { //服務(wù)器的本質(zhì)實(shí)際上就是一個(gè)死循環(huán) char buffer[gnum]; for (;;) { struct sockaddr_in peer; socklen_t len = sizeof(peer); //必填 ssize_t s = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(structsockaddr*)&peer,&len); //成功返回字節(jié)數(shù) } }
我們在使用recvfrom接口的時(shí)候,對于緩沖區(qū)是不用考慮\0的存在的,所以長度是1024-1.然后我們的結(jié)構(gòu)體類型在參數(shù)中需要做強(qiáng)制類型轉(zhuǎn)換,理由與上面同理。下面我們思考讀到數(shù)據(jù)該干什么?我們的目的是實(shí)現(xiàn)一個(gè)udp服務(wù)器用來進(jìn)行簡單的聊天,聊天的時(shí)候要顯示出客戶端的ip和端口號,所以我們這樣設(shè)計(jì):
void start() { //服務(wù)器的本質(zhì)實(shí)際上就是一個(gè)死循環(huán) char buffer[gnum]; for (;;) { struct sockaddr_in peer; socklen_t len = sizeof(peer); //必填 ssize_t s = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len); //成功返回字節(jié)數(shù) //1.數(shù)據(jù)是什么? 2.誰發(fā)的 if (s>0) { buffer[s] = 0; string clientip = inet_ntoa(peer.sin_addr); //1.網(wǎng)絡(luò)序列 2.int->點(diǎn)分十進(jìn)制IP uint16_t clientport = ntohs(peer.sin_port); string message = buffer; cout<<clientip<<"["<<clientport<<"]#"<<message<<endl; } } }
如果讀取數(shù)據(jù)成功,我們先將緩沖區(qū)中最后一個(gè)位置填上\0,這樣我們就可以用string來接收這個(gè)緩沖區(qū)中的字符了,然后我們獲取用戶的IP,由于結(jié)構(gòu)體中的類型是網(wǎng)絡(luò)的,所以我們需要將網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)回主機(jī)字節(jié)序,而這里有一個(gè)接口與我們那會(huì)用的inet_addr正好相反,那就是inet_ntoa接口:
這個(gè)接口可以為完成兩步:1.ntol(struct in_addr) 2.ntol(struct in_addr)->char*
ntol就是hton相反的轉(zhuǎn)換接口。
獲取到string類型的ip后,我們再接收端口號,同樣需要轉(zhuǎn)換,然后我們就打印用戶端ip[端口號]+用戶端發(fā)的消息即可。這樣服務(wù)端的代碼就實(shí)現(xiàn)完成了。
下面我們開始完成客戶端代碼:
首先客戶端必須要有的是服務(wù)端的IP和服務(wù)端的port,所以我們先寫一個(gè)框架:
namespace Client { class udpClient { public: udpClient(const string& serverip,const uint16_t &serverport) :_serverip(serverip) ,_serverport(serverport) { } void InitClient() { } void run() { } ~udpClient() { } private: string _serverip; uint16_t _serverport; }; }
前面我們說過,對于服務(wù)器而言,ip地址是不重要的,只需要端口號就可以啟動(dòng)服務(wù)器,因?yàn)橐话惴?wù)器的IP都是全0,代表任意IP都可以訪問,所以我們的客戶端只需要隨便填一個(gè)IP加上特殊的端口號就可以通信了,那么客戶端內(nèi)部ip和port肯定是必須要有的,明白了這個(gè)知識我們就先實(shí)現(xiàn)一下client.cc的框架:
#include "udpClient.hpp" #include <memory> using namespace Client; static void Usage(string proc) { cout<<"Usage:\n\t"<<proc<<" server_ip server_port\n\n"; } //./udpClient server_ip server_port int main(int argc, char* argv[]) { if (argc!=3) { Usage(argv[0]); exit(1); } string serverip = argv[1]; uint16_t serverport = atoi(argv[2]); unique_ptr<udpClient> ucli(new udpClient(serverip,serverport)); ucli->InitClient(); ucli->run(); return 0; }
這里的原理和我們服務(wù)器寫的一模一樣,我們就直接編寫客戶端代碼:
首先我們客戶端的初始化一定也是需要?jiǎng)?chuàng)建套接字的,既然要?jiǎng)?chuàng)建套接字就必須要有一個(gè)變量接收套接字返回的文件描述符:
udpClient(const string& serverip,const uint16_t &serverport) :_sockfd(-1) ,_serverip(serverip) ,_serverport(serverport) { }
然后我們編寫初始化函數(shù):
void InitClient() { // 1.創(chuàng)建socket _sockfd = socket(AF_INET,SOCK_DGRAM,0); if (_sockfd==-1) { cerr<<"socket error: "<<errno<<" : "<<strerror(errno)<<endl; exit(2); } cout<<"client socket success: "<<" : "<<_sockfd<<endl; //2. client要不要bind(必須要),client要不要明確的bind(不需要,不需要程序員自己bind(由OS自動(dòng)形成端口綁定)) }
我們客戶端的代碼很簡單,相比服務(wù)端客戶端是不需要明確的去bind的,這是因?yàn)榉?wù)端必須要有指定的不能隨意改變的端口,這樣我們的客戶端才能找到服務(wù)端,就像110一樣,110這個(gè)電話是不能隨意更改的,但是對于用戶端自己來講,我自己是什么樣的端口不重要,我只需要通過服務(wù)端的端口訪問服務(wù)端。所以我們一定要注意:客戶端需要bind,但是不需要程序員明確的bind,這里我們自己不bind,操作系統(tǒng)察覺到我們沒有綁定后會(huì)自動(dòng)幫我們綁定,并且每次綁定的端口號都是隨機(jī)的。
下面我們編寫客戶端運(yùn)行的函數(shù),客戶端運(yùn)行很簡單,我們只需要讓客戶端輸入數(shù)據(jù),這樣的話我們服務(wù)端就可以接受到數(shù)據(jù),因?yàn)槲覀兊哪康木褪呛唵蔚木W(wǎng)絡(luò)通信。
對于客戶端發(fā)消息,我們需要認(rèn)識一個(gè)接口:
第一個(gè)參數(shù)是我們創(chuàng)建套接字返回的文件描述符,第二個(gè)參數(shù)和第三個(gè)參數(shù)是一起的,buf是我們發(fā)送的數(shù)據(jù)所在的緩沖區(qū),第三個(gè)參數(shù)是緩沖區(qū)的長度,第四個(gè)參數(shù)是發(fā)送方式,我們還是默認(rèn)填0表示阻塞發(fā)送,有數(shù)據(jù)就發(fā),沒數(shù)據(jù)就等。第五個(gè)參數(shù)和第六個(gè)參數(shù)同樣是輸入型參數(shù),我們客戶端需要提前創(chuàng)建一個(gè)結(jié)構(gòu)體,向里面填充我們客戶端的ip和端口號,然后通過sendto接口發(fā)送到服務(wù)端,然后服務(wù)端就會(huì)接收到我們的數(shù)據(jù)和ip和端口號。
void run() { struct sockaddr_in server; memset(&server,0,sizeof(server)); server.sin_family = AF_INET; server.sin_addr.s_addr = inet_addr(_serverip.c_str()); server.sin_port = htons(_serverport); string message; while (!_quit) { cout<<"Please Enter# "; cin>>message; sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server)); } }
這里我們的客戶端要持續(xù)的輸入所以設(shè)為死循環(huán),quit是我們新增的一個(gè)成員變量:
以上就是我們客戶端的代碼了,實(shí)際上客戶端的代碼非常簡單,下面我們運(yùn)行起來:
運(yùn)行起來后我們可以看到是沒問題的,這里也解釋了為什么說客戶端端口不需要程序員綁定,我們可以看到每次客戶端重新登錄在服務(wù)端顯示的端口號都是不一樣的,因?yàn)檫@是操作系統(tǒng)自動(dòng)指定的端口號,而我們的服務(wù)端的端口號是唯一的,我們客戶端必須輸入服務(wù)端正確的端口號才能訪問服務(wù)端,當(dāng)然小伙伴們也一樣將服務(wù)端的可執(zhí)行程序直接發(fā)給你們的小伙伴,然后讓他們直接通過任意ip+ 你的服務(wù)器端口號來和你進(jìn)行聊天,下面是多人通過網(wǎng)絡(luò)聊天的界面:
總結(jié)
以上就是我們udp服務(wù)器的所有內(nèi)容了,下一篇文章我們將會(huì)把這個(gè)服務(wù)器改造稱為英漢互譯,大型聊天室等好玩的工具。
到此這篇關(guān)于Linux手把手教你實(shí)現(xiàn)udp服務(wù)器的文章就介紹到這了,更多相關(guān)Linux udp服務(wù)器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
圖文詳解Ubuntu搭建Ftp服務(wù)器的方法(包成功)
今天小編就為大家分享一篇關(guān)于圖文詳解Ubuntu搭建Ftp服務(wù)器的方法(包成功),小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-03-03Ubuntu下安裝nvidia顯卡驅(qū)動(dòng)(安裝方式簡單)
這篇文章主要介紹了Ubuntu下安裝nvidia顯卡驅(qū)動(dòng),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05Linux之進(jìn)程狀態(tài)&&進(jìn)程優(yōu)先級詳解
文章介紹了操作系統(tǒng)中進(jìn)程的狀態(tài),包括運(yùn)行狀態(tài)、阻塞狀態(tài)和掛起狀態(tài),并詳細(xì)解釋了Linux下進(jìn)程的具體狀態(tài)及其管理,此外,文章還討論了進(jìn)程的優(yōu)先級、查看和修改進(jìn)程優(yōu)先級的方法,以及并發(fā)相關(guān)的概念和函數(shù)的返回值2025-02-02Linux下實(shí)現(xiàn)UTF-8和GB2312互相轉(zhuǎn)換的方法
下面小編就為大家?guī)硪黄狶inux下實(shí)現(xiàn)UTF-8和GB2312互相轉(zhuǎn)換的方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,祝大家游戲愉快哦2016-12-12如何在Linux服務(wù)上管理Redis的啟動(dòng)、重啟和關(guān)閉
Redis是一個(gè)高性能的開源鍵值對存儲(chǔ)數(shù)據(jù)庫,廣泛用于緩存、會(huì)話管理和實(shí)時(shí)數(shù)據(jù)處理等場景,本文將詳細(xì)介紹如何在Linux系統(tǒng)上啟動(dòng)、重啟和關(guān)閉Redis服務(wù),并提供相關(guān)的配置和故障排除技巧,需要的朋友可以參考下2024-05-05Linux中大內(nèi)存頁Oracle數(shù)據(jù)庫優(yōu)化的方法
這篇文章主要給大家介紹了關(guān)于Linux中大內(nèi)存頁Oracle數(shù)據(jù)庫優(yōu)化的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11