本專欄內容為:Linux學習專欄,分為系統和網絡兩部分。 通過本專欄的深入學習,你可以了解并掌握Linux。
💓博主csdn個人主頁:小小unicorn
?專欄分類:網絡
🚚代碼倉庫:小小unicorn的代碼倉庫🚚
🌹🌹🌹關注我帶你學習編程知識
udpsocket
- 整體框架1(代碼結構)
- 服務端創建套接字
- socket函數
- socket函數屬于什么類型的接口?
- socket函數是被誰調用的?
- socket函數底層做了什么?
- 服務端創建套接字
- 服務端綁定
- bind函數
- struct sockaddr_in結構體
- 如何理解綁定?
- 增加ip地址和端口號
- 字符串IP VS 整數IP
- 整數IP存在的意義
- 字符串IP和整數IP相互轉換的方式
- inet_addr函數
- inet_ntoa函數
- 服務端綁定
- 運行服務器
- recvfrom函數
- 啟動服務器函數
- INADDR_ANY
- 綁定INADDR_ANY的好處
- 更改代碼
- 引入命令行參數
- 整體框架2(代碼結構)
- 客戶端創建套接字
- 關于客戶端的綁定問題
- 啟動客戶端
- 增加服務端IP地址和端口號
- sendto函數
- 啟動客戶端函數
- 引入命令行參數
- 測試:
- 網絡測試:
- 分發客戶端
- 進行網絡測試
整體框架1(代碼結構)
在正式編寫我們的udpsocket之前我們先把準備工作做好將代碼整體框架搭建好。
makefile
udpserve:Main.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm udpserve
對于我們的服務器來說,干兩件事,初始化,初始化完成后就讓它跑起來:
main.cc
#include"UdpServe.hpp"
#include<memory>int main()
{std::unique_ptr<UdpServe> svr(new UdpServe);//初始化svr->Init();//跑起來svr->Run();return 0;
}
udpserver.hpp
#pragma once#include"log.hpp"
class UdpServe
{
public:UdpServe(){}void Init(){}void Run(){}~UdpServe(){}
private:};
跑起來試一下:
能跑成功,接下來就實現我們相關的接口函數即可。
注意:
因為可能需要用到打印信息,也就是需要一個日志功能,我們將引用之前的log日志,具體代碼如下:
log.hpp
#pragma once
#include<iostream>
#include<time.h>
#include<stdarg.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define logFile "log.txt"
class Log
{
public:Log(){printMethod=Screen;path="./log/";}~Log(){}void Enable(int method){printMethod=method;}std::string levelToString(int level){switch(level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}void printLog(int level,const std::string &logtxt){switch(printMethod){case Screen:std::cout<<logtxt<<std::endl;break;case Onefile:printOneFile(logFile,logtxt);break;case Classfile:printClassfie(level,logtxt);break;default:break;}}void printOneFile(const std::string &logname,const std::string &logtxt){std::string _logname=path+logname;int fd=open(_logname.c_str(),O_WRONLY | O_CREAT | O_APPEND,0666);//"log.txt"if(fd<0){return;}write(fd,logtxt.c_str(),logtxt.size());close(fd);}void printClassfie(int level,const std::string &logtxt){std::string filename =logFile;filename += ".";filename += levelToString(level);//"log.txt.Debug/Warning/Fatal"printOneFile(filename,logtxt);}void operator()(int level,const char *format, ...){time_t t=time(nullptr);struct tm *ctime=localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%d-%d-%d %d:%d:%d]",levelToString(level).c_str(),ctime->tm_year+1900,ctime->tm_mon+1,ctime->tm_mday,ctime->tm_hour,ctime->tm_min,ctime->tm_sec);va_list s;va_start(s,format);char rightbuffer[SIZE];vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);va_end(s);//格式:默認部分+自定義部分char logtxt[SIZE*2];snprintf(logtxt,sizeof(logtxt),"%s %s",leftbuffer,rightbuffer);//print("%s",logtxt);//暫時打印printLog(level,logtxt);}private:int printMethod;std::string path;
};
服務端創建套接字
我們把服務器封裝成一個類,當我們定義出一個服務器對象后需要馬上初始化服務器,而初始化服務器需要做的第一件事就是創建套接字。
socket函數
創建套接字的函數叫做socket
,該函數的函數原型如下:
int socket(int domain,int type,int protocol)
參數解釋:
domain
:創建套接字的域或者叫做協議家族,也就是創建套接字的類型,該參數就相當于struct sockaddr
結構的前16位。如果是本地通信就設置為AF_UNIX
,如果是網絡通信就設置為AF_INET
(IPv4)或者AF_INET6
(IPv6).type
:創建套接字時所需要的服務類型。其中最常見的服務類型是SOCK_STREAM
和SOCK_DGRAM
,如果是基于UDP的網絡通信,我們采用SOCK_DGRAM
,叫做用戶數據報服務,如果是基于TCP的網絡通信,我們采用的就是SOCK_STREAM
,叫做流式套接字,提供的是流式服務。protocol
:創建套接字的協議類型。你可以指明它為TCP或者UDP,但該字段一般直接設置為0就可以了。設置為0表示的就是默認,此時會根據傳入的前兩個參數自動 推導出你最終需要使用的是哪種類型。
返回值解釋:
套接字創建成功返回一個文件描述符,創建失敗返回-1;同時錯誤碼會被設置。
同時需要包含兩個頭文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
socket函數屬于什么類型的接口?
網絡協議棧是分層的,按照TCP/UDP四層模型來說,自頂向下依次是應用層,傳輸層,網絡層和數據鏈路層,而我們現在所寫的代碼都叫做用戶級代碼,也就是我們是在應用層編寫代碼,因此我們調用的實際是下三層的接口,而傳輸層和網絡層都是在操作系統內完成的,也就意味著我們在應用層調用的接口都叫做系統調用接口。
socket函數是被誰調用的?
socket這個函數是被程序調用的,但并不是被程序在編碼上直接調用的,而是程序編碼形成的可執行程序運行起來變成進程,當這個進程被CPU調度執行到socket函數時,然后才會執行創建套接字的代碼,也就是說socket函數是被進程所調用的。
socket函數底層做了什么?
socket函數是被進程所調用,而每一個進程在系統層面上都有一個進程地址空間PCB(task_struct
),文件描述符表(file_struct
)以及對應打卡的各種文件,而文件描述符表里面包含了一個數組(fd_array
),其中數組中的0,1,2下標依次對應的就是標準輸入,標準輸出和標準錯誤。
當我們調用socket函數創建套接字時,實際相當于我們打開的一個“網絡文件”,打開后在內核層面上就形成了一個對應的struct file
結構體,同時該結構體被連入到了該進程對應的文件雙鏈表,并將該結構體的首地址填入到了fd_array
數組中下標為3的位置,此時fd_array
數組中下標為3的指針就指向了這個打開的“網絡文件”,最后3號文件描述符作為socket函數的返回值返回給了客戶。
其中每一個struct file
結構體中包含的就是對應打開文件各種信息,比如文件的屬性信息,操作方法以及文件緩沖區等。其中文件對應的屬性在內核中是由struct inode
結構體維護的,而文件對應的操作方法實際就是一堆的函數指針(比如read*
和write*
)在內核當中就是由struct file_operations
結構體來維護的。而文件緩沖區對于打開的普通文件來說對應的一般是磁盤,但對于現在打開的“網絡文件”來說,這里的文件緩沖區對用的就是網卡。
對于一般的普通文件來說,當用戶通過文件描述符將數據寫到文件緩沖區,然后再把數據刷到磁盤上就完成了數據的寫入操作,而對于現在socket函數打開的“網絡文件”來說,當用戶將數據寫到文件緩沖區后,操作系統會定期將數據刷到網卡上,而網卡則是負責數據發送的,因此數據最終就發送到了網絡當中。
服務端創建套接字
當我們在進行初始化服務器創建套接字時,就是調用socket函數創建套接字,創建套接字時我們需要填入的協議家族就是AF_INET
,因為我們要進行的是網絡通信,而我們需要的服務類型就是SOCK_DGRAM
,因為我們現在編寫的UDP服務器是面向數據報的,而第三個參數設置為0即可。
UdpServer.hpp
#pragma once
#include<sys/types.h>
#include<sys/socket.h>
#include<iostream>
#include<string>
#include<strings.h>
#include<cstring>
#include"log.hpp"
//創建日志功能
Log lg;
//枚舉常量
enum
{SOCKET_ERR=1,BIND_ERR
};
class UdpServer
{
public:void Init(){//構造函數UdpServe(){}//1.創建udp socketsockfd_=socket(AF_INET,SOCK_DGRAM,0);//PF_INET//網絡文件描述符小于0,打印錯誤信息if(sockfd_<0){//打印錯誤信息lg(Fatal,"socket create error,sockfd:%d",sockfd_);//以錯誤信息退出exit(SOCKET_ERR);}lg(Info,"socket creat success, sockfd: %d",sockfd_);}~UdpServe(){if(sockfd_>0)//關閉close(sockfd_);}
private:int sockfd_; //網絡文件描述符
};
注意: 當析構服務器時,我們可以將sockfd對應的文件進行關閉,但實際上不進行該操作也行,因為一般服務器運行后是就不會停下來的。(也可以這樣理解,因為服務器一天24小時都在跑,因此他在代碼里面是一個死循環)
這里我們可以做一個簡單的測試,看看套接字是否創建成功。
#include"UdpServer.hpp"
#include<memory>
int main()
{std::unique_ptr<UdpServer> svr(new UdpServer);//初始化svr->Init();//跑起來svr->Run();return 0;
}
運行程序后可以看到套接字是創建成功的,對應獲取到的文件描述符就是3,這也很好理解,因為0、1、2默認被標準輸入流、標準輸出流和標準錯誤流占用了,此時最小的、未被利用的文件描述符就是3。
服務端綁定
現在套接字已經創建成功了,但作為一款服務器來講,如果只是把套接字創建好了,那我們也只是在系統層面上打開了一個文件,操作系統將來并不知道是要將數據寫入到磁盤還是刷到網卡,此時該文件還沒有與網絡關聯起來
由于現在編寫的是不面向連接的UDP服務器,所以初始化服務器要做的第二件事就是綁定。
bind函數
綁定的函數叫做bind,該函數的函數原型如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
參數解釋:
sockfd
:綁定的文件的文件描述符。也就是我們創建套接字時獲取到的文件描述符。addr
:網絡相關的屬性信息,包括協議家族、IP地址、端口號等。addrlen
:傳入的addr結構體的長度。
返回值解釋:
綁定成功返回0,綁定失敗返回-1,同時錯誤碼會被設置。
struct sockaddr_in結構體
在綁定時需要將網絡相關的屬性信息填充到一個結構體當中,然后將該結構體作為bind函數的第二個參數進行傳入,這實際就是struct sockaddr_in結構體。
我們轉到socket_in的定義:
可以看到,struct sockaddr_in當中的成員如下:
sin_family
:表示協議家族。sin_port
:表示端口號,是一個16位的整數。sin_addr
:表示IP地址,是一個32位的整數。
剩下的字段一般不做處理,當然你也可以進行初始化。
其中sin_addr
的類型是struct in_addr
,實際該結構體當中就只有一個成員,該成員就是一個32位的整數,IP地址實際就是存儲在這個整數當中的。
如何理解綁定?
在進行綁定的時候需要將IP地址和端口號告訴對應的網絡文件,此時就可以改變網絡文件當中文件操作函數的指向,將對應的操作函數改為對應網卡的操作方法,此時讀數據和寫數據對應的操作對象就是網卡了,所以綁定實際上就是將文件和網絡關聯起來。
增加ip地址和端口號
由于綁定時需要用到IP地址和端口號,因此我們需要在服務器類當中引入IP地址和端口號,在創建服務器對象時需要傳入對應的IP地址和端口號,此時我們就可以根據傳入的IP地址和端口號對對應的成員進行初始化。
同時我們之前也說過,服務器要一天二十四小時工作,我們定義一個變量來記錄服務器的工作情況,會在Run()函數接口用到,這里我們直接先定義好,在Run()函數那就直接拿來用啦。
同時我們給port和ip給一個缺省值:
class UdpServer
{
// 初始化 缺省值
//端口號默認設為8080
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
public://構造函數UdpServe(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0),ip_(ip),port_(port),isrunning_(false)~UdpServer(){if (_sockfd >= 0){close(_sockfd);}};
private:int sockfd_; //網絡文件描述符std::string ip_;//任意地址綁定uint16_t port_;//表明服務器進程的端口號bool isrunning_;//服務器的狀態
};
注意: 雖然這里端口號定義為整型,但由于端口號是16位的,因此我們實際只會用到它的低16位。
字符串IP VS 整數IP
IP地址的表現形式有兩種:
- 字符串IP:類似于
192.168.233.123
這種字符串形式的IP地址,叫做基于字符串的點分十進制IP地址。 - 整數IP:IP地址在進行網絡傳輸時所用的形式,用一個32位的整數來表示IP地址。
整數IP存在的意義
網絡傳輸數據時是寸土寸金的,如果我們在網絡傳輸時直接以基于字符串的點分十進制IP的形式進行IP地址的傳送,那么此時一個IP地址至少就需要15個字節,但實際并不需要耗費這么多字節。
IP地址實際可以劃分為四個區域,其中每一個區域的取值都是0~255,而這個范圍的數字只需要用8個比特位就能表示,因此我們實際只需要32個比特位就能夠表示一個IP地址。其中這個32位的整數的每一個字節對應的就是IP地址中的某個區域,我們將IP地址的這種表示方法稱之為整數IP,此時表示一個IP地址只需要4個字節。
因為采用整數IP的方案表示一個IP地址只需要4個字節,并且在網絡通信也能表示同樣的含義,因此在網絡通信時就沒有用字符串IP而用的是整數IP,因為這樣能夠減少網絡通信時數據的傳送。
字符串IP和整數IP相互轉換的方式
轉換的方式有很多,比如我們可以定義一個位段A,位段A當中有四個成員,每個成員的大小都是8個比特位,這四個成員就依次表示IP地址的四個區域,一共32個比特位。
然后我們再定義一個聯合體IP,該聯合體當中有兩個成員,其中一個是32位的整數,其代表的就是整數IP,還有一個就是位段A類型的成員,其代表的就是字符串IP。
由于聯合體的空間是成員共享的,因此我們設置IP和讀取IP的方式如下:
- 當我們想以整數IP的形式設置IP時,直接將其賦值給聯合體的第一個成員就行了。
- 當我們想以字符串IP的形式設置IP時,先將字符串分成對應的四部分,然后將每部分轉換成對應的二進制序列依次設置到聯合體中第二個成員當中的p1、p2、p3和p4就行了。
- 當我們想取出整數IP時,直接讀取聯合體的第一個成員就行了。
- 當我們想取出字符串IP時,依次獲取聯合體中第二個成員當中的p1、p2、p3和p4,然后將每一部分轉換成字符串后拼接到一起就行了。
注意: 在操作系統內部實際用的就是位段和枚舉,來完成字符串IP和整數IP之間的相互轉換的
轉換大小端函數:
inet_addr函數
實際在進行字符串IP和整數IP的轉換時,我們不需要自己編寫轉換邏輯,系統已經為我們提供了相應的轉換函數,我們直接調用即可。
將字符串IP轉換成整數IP的函數叫做inet_addr,該函數的函數原型如下:
in_addr_t inet_addr(const char *cp);
該函數使用起來非常簡單,我們只需傳入待轉換的字符串IP,該函數返回的就是轉換后的整數IP。除此之外,inet_aton函數也可以將字符串IP轉換成整數IP,不過該函數使用起來沒有inet_addr簡單。
inet_ntoa函數
將整數IP轉換成字符串IP的函數叫做inet_ntoa,該函數的函數原型如下:
char *inet_ntoa(struct in_addr in);
需要注意的是,傳入inet_ntoa函數的參數類型是in_addr
,因此我們在傳參時不需要選中in_addr
結構當中的32位的成員傳入,直接傳入in_addr
結構體即可。
服務端綁定
套接字創建完畢后我們就需要進行綁定了,但在綁定之前我們需要先定義一個struct sockaddr_in
結構,將對應的網絡屬性信息填充到該結構當中。由于該結構體當中還有部分選填字段,因此我們最好在填充之前對該結構體變量里面的內容進行清空,然后再將協議家族、端口號、IP地址等信息填充到該結構體變量當中。
需要注意的是,在發送到網絡之前需要將端口號設置為網絡序列,由于端口號是16位的,因此我們需要使用前面說到的htons函數將端口號轉為網絡序列。此外,由于網絡當中傳輸的是整數IP,我們需要調用inet_addr
函數將字符串IP轉換成整數IP,然后再將轉換后的整數IP進行設置。
當網絡屬性信息填充完畢后,由于bind函數提供的是通用參數類型,因此在傳入結構體地址時還需要將struct sockaddr_in*
強轉為struct sockaddr*
類型后再進行傳入。
#pragma once
#include<sys/types.h>
#include<sys/socket.h>
#include<iostream>
#include<string>
#include<strings.h>
#include<cstring>
//創建日志功能
Log lg;
//枚舉常量
enum
{SOCKET_ERR=1,BIND_ERR
};
/// 初始化 缺省值
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;class UdpServer
{
public:void Init(){//構造函數UdpServe(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0),ip_(ip),port_(port),isrunning_(false){}//1.創建udp socketsockfd_=socket(AF_INET,SOCK_DGRAM,0);//PF_INET//網絡文件描述符小于0,打印錯誤信息if(sockfd_<0){//打印錯誤信息lg(Fatal,"socket create error,sockfd:%d",sockfd_);//以錯誤信息退出exit(SOCKET_ERR);}lg(Info,"socket creat success, sockfd: %d",sockfd_);//2. bind socket 綁定套接字struct sockaddr_in local;//全部置為0;bzero(&local,sizeof(local));//協議家族local.sin_family=AF_INET;//端口號//需要保證我的端口號是網絡字節序列,因為該端口號是要給對方發送的//將主機序列轉化成網絡字節序列,//如果是大端,不用管,如果是小端將小端序列轉化成大端序列local.sin_port=htons(port_);//1. string -> uint32_t 2. uint32_t必須是網絡序列的 // ??//inet_addr直接將字符串轉化成網絡序列local.sin_addr.s_addr=inet_addr(ip_.c_str());// local.sin_addr.s_addr = htonl(INADDR_ANY);////綁定://對local進行強轉if(bind(sockfd_,(const struct sockaddr*)&local,sizeof(local))<0){//綁定失敗//打印錯誤信息lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}//方便進行查看:lg(Info,"bind success, errno: %d, err string: %s", errno, strerror(errno));}~UdpServe(){if(sockfd_>0)//關閉close(sockfd_);}
private:int sockfd_; //網絡文件描述符std::string ip_;//任意地址綁定uint16_t port_;//表明服務器進程的端口號
};
看一下有沒有綁定成功:
這里我們能看到綁定是成功了的。
運行服務器
UDP服務器的初始化就只需要創建套接字和綁定就行了,當服務器初始化完畢后我們就可以啟動服務器了。
服務器實際上就是在周而復始的為我們提供某種服務,服務器之所以稱為服務器,是因為服務器運行起來后就永遠不會退出,因此服務器實際執行的是一個死循環代碼。由于UDP服務器是不面向連接的,因此只要UDP服務器啟動后,就可以直接讀取客戶端發來的數據。
recvfrom函數
UDP服務器讀取數據的函數叫做recvfrom
,該函數的函數原型如下:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
參數解釋:
sockfd
:對應操作的文件描述符。表示從該文件描述符索引的文件當中讀取數據。buf
:讀取數據的存放位置。len
:期望讀取數據的字節數。flags
:讀取的方式。一般設置為0,表示阻塞讀取。src_addr
:對端網絡相關的屬性信息,包括協議家族、IP地址、端口號等。addrlen
:調用時傳入期望讀取的src_addr結構體的長度,返回時代表實際讀取到的src_addr結構體的長度,這是一個輸入輸出型參數。
返回值解釋:
讀取成功返回實際讀取到的字節數,讀取失敗返回-1,同時錯誤碼會被設置。
注意:
1.由于UDP是不面向連接的,因此我們除了獲取到數據以外還需要獲取到對端網絡相關的屬性信息,包括IP地址和端口號等。
2. 在調用recvfrom讀取數據時,必須將addrlen
設置為你要讀取的結構體對應的大小。
3. 由于recvfrom函數提供的參數也是struct sockaddr*
類型的,因此我們在傳入結構體地址時需要將struct sockaddr_in*
類型進行強轉。
啟動服務器函數
現在服務端通過recvfrom函數讀取客戶端數據,我們可以先將讀取到的數據當作字符串看待,將讀取到的數據的最后一個位置設置為'\0'
,此時我們就可以將讀取到的數據進行輸出,同時我們也可以將獲取到的客戶端的IP地址和端口號也一并進行輸出。
需要注意的是,我們獲取到的客戶端的端口號此時是網絡序列,我們需要調用ntohs函數將其轉為主機序列再進行打印輸出。同時,我們獲取到的客戶端的IP地址是整數IP,我們需要通過調用inet_ntoa
函數將其轉為字符串IP再進行打印輸出。
class UdpServer
{
const int size = 1024;
public://將socket跑起來 運行void Run()//void Run(func_t func){isrunning_ = true;//定義一個緩沖區char inbuffer[size];while(isrunning_){//客戶端結構體struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);if(n < 0){//創建失敗lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}//將信息讀到后,當成字符串來看inbuffer[n] = 0;printf("client say: %s\n", inbuffer);//充當一次數據的處理std::string info = inbuffer;std::string echo_string="server echo# "+info;//std::string echo_string = func(info);//發送給對方sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);}}
private:int sockfd_; //網絡文件描述符std::string ip_;//任意地址綁定uint16_t port_;//表明服務器進程的端口號bool isrunning_;//服務器的狀態
};
注意: 如果調用recvfrom
函數讀取數據失敗,我們可以打印一條提示信息,但是不要讓服務器退出,服務器不能因為讀取某一個客戶端的數據失敗就退出。
雖然現在客戶端代碼還沒有編寫,但是我們可以通過netstat
命令來查看當前網絡的狀態,這里我們可以選擇攜帶nlup
選項。
netstat
常用選項說明:
-n
:直接使用IP地址,而不通過域名服務器。
-l
:顯示監控中的服務器的Socket。
-t
:顯示TCP傳輸協議的連線狀況。
-u
:顯示UDP傳輸協議的連線狀況。
-p
:顯示正在使用Socket的程序識別碼和程序名稱。
此時你就能查看到對應網絡相關的信息,在這些信息中程序名稱為./udp_server
的那一行顯示的就是我們運行的UDP服務器的網絡信息。
你可以嘗試去掉-n
選項再查看,此時原本顯示IP地址的地方就變成了對應的域名服務器。
其中netstat
命令顯示的信息中,Proto
表示協議的類型,Recv-Q
表示網絡接收隊列,Send-Q
表示網絡發送隊列,Local Address
表示本地地址,Foreign Address
表示外部地址,State
表示當前的狀態,PID
表示該進程的進程ID,Program name
表示該進程的程序名稱。
其中Foreign Address
寫成0.0.0.0:*
表示任意IP地址、任意的端口號的程序都可以訪問當前進程。
但是這個能跑起來僅僅是一個巧合,這里面有坑,為什么呢?因為我們ip運行 用的是我們默認的值,最后字符串轉成4字節會變成0值。
但是我們想讓服務器啟動的時候綁定我們的ip地址,看一下效果:
顯示綁定失敗:
這是為什么呢?
其實這個代碼在虛擬機上其實是可以運行的,但是云服務器就不行,這是因為服務器直接禁止bind公網ip,因此我們通常讓我們的ip為0值,0值意思就是凡是給我這臺主機的數據,我們都要根據端口號向上交付。
INADDR_ANY
由于云服務器的IP地址是由對應的云廠商提供的,這個IP地址并不一定是真正的公網IP,這個IP地址是不能直接被綁定的,如果需要讓外網訪問,此時我們需要bind 0。系統當當中提供的一個INADDR_ANY
,這是一個宏值,它對應的值就是0。
因此如果我們需要讓外網訪問,那么在云服務器上進行綁定時就應該綁定INADDR_ANY
,此時我們的服務器才能夠被外網訪問。
綁定INADDR_ANY的好處
當一個服務器的帶寬足夠大時,一臺機器接收數據的能力就約束了這臺機器的IO效率,因此一臺服務器底層可能裝有多張網卡,此時這臺服務器就可能會有多個IP地址,但一臺服務器上端口號為8081的服務只有一個。這臺服務器在接收數據時,這里的多張網卡在底層實際都收到了數據,如果這些數據也都想訪問端口號為8081的服務。此時如果服務端在綁定的時候是指明綁定的某一個IP地址,那么此時服務端在接收數據的時候就只能從綁定IP對應的網卡接收數據。而如果服務端綁定的是INADDR_ANY
,那么只要是發送給端口號為8081的服務的數據,系統都會可以將數據自底向上交給該服務端。
因此服務端綁定INADDR_ANY
這種方案也是強烈推薦的方案,所有的服務器具體在操作的時候用的也就是這種方案。
當然,如果你既想讓外網訪問你的服務器,但你又指向綁定某一個IP地址,那么就不能用云服務器,此時可以選擇使用虛擬機或者你自定義安裝的Linux操作系統,那個IP地址就是支持你綁定的,而云服務器是不支持的。
更改代碼
因此,如果想要讓外網訪問我們的服務,我們這里就需要將服務器類當中IP地址相關的代碼去掉,而在填充網絡相關信息設置struct sockaddr_in結構體時,將設置的IP地址改為INADDR_ANY就行了。
class UdpServer
{
public:void Init(){//1.創建udp socketsockfd_=socket(AF_INET,SOCK_DGRAM,0);//PF_INET//網絡文件描述符小于0,打印錯誤信息if(sockfd_<0){//打印錯誤信息lg(Fatal,"socket create error,sockfd:%d",sockfd_);//以錯誤信息退出exit(SOCKET_ERR);}lg(Info,"socket creat success, sockfd: %d",sockfd_);//2. bind socket 綁定套接字struct sockaddr_in local;//全部置為0;bzero(&local,sizeof(local));//協議家族local.sin_family=AF_INET;//端口號//需要保證我的端口號是網絡字節序列,因為該端口號是要給對方發送的//將主機序列轉化成網絡字節序列,//如果是大端,不用管,如果是小端將小端序列轉化成大端序列local.sin_port=htons(port_);//1. string -> uint32_t 2. uint32_t必須是網絡序列的 // ??//inet_addr直接將字符串轉化成網絡序列//local.sin_addr.s_addr=inet_addr(ip_.c_str());local.sin_addr.s_addr = htonl(INADDR_ANY);////綁定://對local進行強轉if(bind(sockfd_,(const struct sockaddr*)&local,sizeof(local))<0){//綁定失敗//打印錯誤信息lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}//方便進行查看:lg(Info,"bind success, errno: %d, err string: %s", errno, strerror(errno));}
private:int sockfd_; //網絡文件描述符std::string ip_;//任意地址綁定uint16_t port_;//表明服務器進程的端口號bool isrunning_;//服務器的狀態
};
此時當我們再重新編譯運行服務器時就不會綁定失敗了,并且此時當我們再用netstat命令查看時會發現,該服務器的本地IP地址變成了0.0.0.0,這就意味著該UDP服務器可以在本地讀取任何一張網卡里面的數據。
引入命令行參數
鑒于構造服務器時需要傳入IP地址和端口號,我們這里可以引入命令行參數。此時當我們運行服務器時在后面跟上對應的IP地址和端口號即可。
由于云服務器的原因,后面實際不需要傳入IP地址,因此在運行服務器的時候我們只需要傳入端口號即可
int main(int argc,char*argv[])
{if(argc!=2){Usage(argv[0]);exit(0);}uint16_t port=std::stoi(argv[1]);std::unique_ptr<UdpServe> svr(new UdpServe(port));//初始化svr->Init();//跑起來svr->Run();return 0;
}
需要注意的是,agrv數組里面存儲的是字符串,而端口號是一個整數,因此需要使用atoi函數將字符串轉換成整數。然后我們就可以用這個IP地址和端口號來構造服務器了,服務器構造完成并初始化后就可以調用Run()函數啟動服務器了。
此時帶上端口號運行程序就可以看到套接字創建成功、綁定成功,現在服務器就在等待客戶端向它發送數據。
整體框架2(代碼結構)
服務端寫好后,我們為方便測試,我們將在寫一個客戶端。因此代碼整體結構就要進行微調:
Makefile
.PHONY:all
all:udpserver udpclient
udpserver:Main.ccg++ -o $@ $^ -std=c++11
udpclient:ClientMain.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f udpserver udpclient
ClientMain.cc
#include"UdpClient.hpp"
#include<memory>
int main()
{UdpClient* clt = new UdpClient(server_ip, server_port);clt->InitClient();clt->Run();return 0;
}
客戶端創建套接字
同樣的,我們把客戶端也封裝成一個類,當我們定義出一個客戶端對象后也是需要對其進行初始化,而客戶端在初始化時也需要創建套接字,之后客戶端發送數據或接收數據也就是對這個套接字進行操作。
客戶端創建套接字時選擇的協議家族也是AF_INET
,需要的服務類型也是SOCK_DGRAM
,當客戶端被析構時也可以選擇關閉對應的套接字。與服務端不同的是,客戶端在初始化時只需要創建套接字就行了,而不需要進行綁定操作。
#pragma once
//兩個socket頭文件
#include<sys/types.h>
#include<sys/socket.h>
#include<iostream>
#include<string>
#include<strings.h>
#include<cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include"log.hpp"
//創建日志功能
Log lg;
//枚舉常量:
enum
{SOCKET_ERR=1,BIND_ERR
};
class UdpClient
{
public:void InitClient(){//創建套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){//打印錯誤信息lg(Fatal,"socket create error,sockfd:%d",sockfd_);//以錯誤信息退出exit(SOCKET_ERR);}lg(Info,"socket creat success, sockfd: %d",sockfd_);}~UdpClient(){if (_sockfd >= 0){close(_sockfd);}}
private:int sockfd_; //文件描述符
};
關于客戶端的綁定問題
首先,由于是網絡通信,通信雙方都需要找到對方,因此服務端和客戶端都需要有各自的IP地址和端口號,只不過服務端需要進行端口號的綁定,而客戶端不需要。
因為服務器就是為了給別人提供服務的,因此服務器必須要讓別人知道自己的IP地址和端口號,IP地址一般對應的就是域名,而端口號一般沒有顯示指明過,因此服務端的端口號一定要是一個眾所周知的端口號,并且選定后不能輕易改變,否則客戶端是無法知道服務端的端口號的,這就是服務端要進行綁定的原因,只有綁定之后這個端口號才真正屬于自己,因為一個端口只能被一個進程所綁定,服務器綁定一個端口就是為了獨占這個端口。
而客戶端在通信時雖然也需要端口號,但客戶端一般是不進行綁定的,客戶端訪問服務端的時候,端口號只要是唯一的就行了,不需要和特定客戶端進程強相關。
如果客戶端綁定了某個端口號,那么以后這個端口號就只能給這一個客戶端使用,就是這個客戶端沒有啟動,這個端口號也無法分配給別人,并且如果這個端口號被別人使用了,那么這個客戶端就無法啟動了。所以客戶端的端口只要保證唯一性就行了,因此客戶端端口可以動態的進行設置,并且客戶端的端口號不需要我們來設置,當我們調用類似于sendto這樣的接口時,操作系統會自動給當前客戶端獲取一個唯一的端口號。
也就是說,客戶端每次啟動時使用的端口號可能是變化的,此時只要我們的端口號沒有被耗盡,客戶端就永遠可以啟動。
啟動客戶端
增加服務端IP地址和端口號
作為一個客戶端,它必須知道它要訪問的服務端的IP地址和端口號,因此在客戶端類當中需要引入服務端的IP地址和端口號,此時我們就可以根據傳入的服務端的IP地址和端口號對對應的成員進行初始化。
class UdpClient
{
public://構造函數UdpClient(std::string server_ip, int server_port):sockfd_(-1),server_port_(server_port),server_ip_(server_ip){}void InitClient(){//創建套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){//打印錯誤信息lg(Fatal,"socket create error,sockfd:%d",sockfd_);//以錯誤信息退出exit(SOCKET_ERR);}lg(Info,"socket creat success, sockfd: %d",sockfd_);}~UdpClient(){if (_sockfd >= 0){close(_sockfd);}}
private:int sockfd_; //文件描述符int server_port_; //服務端端口號std::string server_ip_; //服務端IP地址
};
當客戶端初始化完畢后我們就可以將客戶端運行起來,由于客戶端和服務端在功能上是相互補充的,既然服務器是在讀取客戶端發來的數據,那么客戶端就應該想服務端發送數據。
sendto函數
UDP客戶端發送數據的函數叫做sendto
,該函數的函數原型如下:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
參數解釋:
sockfd
:對應操作的文件描述符。表示將數據寫入該文件描述符索引的文件當中。buf
:待寫入數據的存放位置。len
:期望寫入數據的字節數。flags
:寫入的方式。一般設置為0,表示阻塞寫入。dest_addr
:對端網絡相關的屬性信息,包括協議家族、IP地址、端口號等。addrlen
:傳入dest_addr
結構體的長度。
返回值解釋:
寫入成功返回實際寫入的字節數,寫入失敗返回-1,同時錯誤碼會被設置。
注意:
由于UDP不是面向連接的,因此除了傳入待發送的數據以外還需要指明對端網絡相關的信息,包括IP
地址和端口號等。
由于sendto
函數提供的參數也是struct sockaddr*
類型的,因此我們在傳入結構體地址時需要將struct sockaddr_in*
類型進行強轉。
啟動客戶端函數
現在客戶端要發送數據給服務端,我們可以讓客戶端獲取用戶輸入,不斷將用戶輸入的數據發送給服務端。
需要注意的是,客戶端中存儲的服務端的端口號此時是主機序列,我們需要調用htons
函數將其轉為網絡序列后再設置進struct sockaddr_in
結構體。同時,客戶端中存儲的服務端的IP地址是字符串IP,我們需要通過調用inet_addr
函數將其轉為整數IP后再設置進struct sockaddr_in
結構體。
class UdpClient
{
public:void Run(){std::string message;//std::string msg;struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;//主機轉網絡server.sin_port = htons(server_port_); server.sin_addr.s_addr = inet_addr(server_ip_.c_str());socklen_t len = sizeof(server);char buffer[1024];while (true){std::cout << "Please Enter@ ";getline(std::cin, message);// std::cout << message << std::endl;// 1. 數據 2. 給誰發sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd_, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}}}
private:int sockfd_; //文件描述符int server_port_; //服務端端口號std::string server_ip_; //服務端IP地址
};
引入命令行參數
鑒于構造客戶端時需要傳入對應服務端的IP地址和端口號,我們這里也可以引入命令行參數。當我們運行客戶端時直接在后面跟上對應服務端的IP地址和端口號即可。
#include"UdpClient.hpp"
#include<memory>
int main(int argc, char* argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}std::string server_ip = argv[1];int server_port = atoi(argv[2]);UdpClient* clt = new UdpClient(server_ip, server_port);clt->InitClient();clt->Run();return 0;
}
需要注意的是,argv數組里面存儲的是字符串,而端口號是一個整數,因此需要使用atoi函數將字符串轉換成整數。然后我們就可以用這個IP地址和端口號來構造客戶端了,客戶端構造完成并初始化后就可以調用Run()函數啟動客戶端了。
測試:
整體寫完后,我們可以進行測試:
我們復制會話,讓左面跑我們的客戶端,右面跑我們的服務器:
先進行編譯,然后啟動我們的服務器,我們默認端口號為8080:
發現服務器此時已經啟動起來了,接下來啟動我們的客戶端:
小編的Ip地址為101.42.156.77.
這里我們從客戶端輸入,服務端能正常顯示。
網絡測試:
其實剛才已經我們進行了網絡測試,接下來我們可以將生成的客戶端的可執行程序發送給你的其他朋友,進行網絡級別的測試。為了保證程序在你們的機器是嚴格一致的,可以選擇在編譯客戶端時攜帶-static選項進行靜態編譯。
此時由于客戶端是靜態編譯的,可以看到生成的客戶端的可執行程序要比服務端大得多。
分發客戶端
此時我們可以先使用sz命令將該客戶端可執行程序下載到本地機器,然后將該程序發送給你的朋友。而我們分發客戶端的過程實際上就是我們在網上下載各種PC端軟件的過程,我們下軟件下的實際上就是客戶端的可執行程序,而與之對應的服務端就在Linux服務器上部署著。
當你的朋友收到這個客戶端的可執行程序后,可以通過rz命令或拖拽的方式將這個可執行程序上傳到他的云服務器上,然后通過chmod命令給該文件加上可執行權限。
點擊rz回車將我們的客戶端添加進去,此時我們的udpclient是默認沒有可執行權限的,因此我們可以按照一下命令:
chomd +x udpclient
我們加上可執行權限:
進行網絡測試
此時你先把你的服務器啟動起來,然后你的朋友將你的IP地址和端口號作為命令行參數運行客戶端,就可以訪問你的服務器了。