一、引入
在上一章【網(wǎng)絡(luò)編程】socket套接字中我們講述了TCP/UDP協(xié)議,這一篇就是簡單實現(xiàn)一個UDP協(xié)議的網(wǎng)絡(luò)服務(wù)器。
我們也講過其實網(wǎng)絡(luò)通信的本質(zhì)就是進程間通信。而進程間通信無非就是讀和寫(IO)。
所以現(xiàn)在我們就要寫一個服務(wù)端(server)接收數(shù)據(jù),客戶端(client)發(fā)送數(shù)據(jù)。
二、服務(wù)端實現(xiàn)
通過上一章的介紹,要通信首先需要有IP地址,和綁定端口號。
uint16_t _port;
std::string _ip;
2.1 創(chuàng)建套接字socket
在通信之前要先把網(wǎng)卡文件打開。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
RETURN VALUE
On success, a file descriptor for the new socket is returned.
On error, -1 is returned, and errno is set appropriately.
這個函數(shù)的作用是打開一個文件,把文件和網(wǎng)卡關(guān)聯(lián)起來。
參數(shù)介紹:
domain
:一個域,標識了這個套接字的通信類型(網(wǎng)絡(luò)或者本地)。
只用關(guān)注上面兩個類,第一個AF_UNIX
表示本地通信,而AF_INET
表示網(wǎng)絡(luò)通信。type
:套接字提供服務(wù)的類型。
這一章我們講的式UDP,所以使用SOCK_DGRAM
。protocol
:想使用的協(xié)議,默認為0即可,因為前面的兩個參數(shù)決定了,就已經(jīng)決定了是TCP還是UDP協(xié)議了。
返回值:
成功則返回打開的文件描述符(指向網(wǎng)卡文件),失敗返回-1。
而從這里我們就聯(lián)想到系統(tǒng)中的文件操作,未來各種操作都要通過這個文件描述符,所以在服務(wù)端類中還需要一個成員變量表示文件描述符。
static const std::string defaultip = "0.0.0.0";// 默認IP
class UDPServer
{
public:
UDPServer(const uint16_t& port, const std::string ip = defaultip)
: _port(port)
, _ip(ip)
, _sockfd(-1)
{}
void InitServer()
{
// 創(chuàng)建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
}
private:
uint16_t _port;
std::string _ip;
int _sockfd;
};
創(chuàng)建完套接字后我們還需要綁定IP和端口號。
2.2 綁定bind
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
RETURN VALUE
Upon successful completion, bind() shall return 0;
otherwise, -1 shall be returned and errno set to indicate the error.
參數(shù)介紹:
socket
:創(chuàng)建套接字的返回值。address
:通用結(jié)構(gòu)體(上一章【網(wǎng)絡(luò)編程】socket套接字有詳細介紹)。address_len
:傳入結(jié)構(gòu)體的長度。
所以我們要先定義一個sockaddr_in
結(jié)構(gòu)體填充數(shù)據(jù),在傳遞進去。
struct sockaddr_in {
short int sin_family; // 地址族,一般為AF_INET或PF_INET
unsigned short int sin_port; // 端口號,網(wǎng)絡(luò)字節(jié)序
struct in_addr sin_addr; // IP地址
unsigned char sin_zero[8]; // 用于填充,使sizeof(sockaddr_in)等于16
};
創(chuàng)建結(jié)構(gòu)體后要先清空數(shù)據(jù)(初始化),我們可以用memset,系統(tǒng)也提供了接口:
#include <strings.h>
void bzero(void *s, size_t n);
填充端口號的時候要注意端口號是兩個字節(jié)的數(shù)據(jù),涉及到大小端問題。
大小端轉(zhuǎn)化接口:
#include <arpa/inet.h>
// 主機序列轉(zhuǎn)網(wǎng)絡(luò)序列
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
// 網(wǎng)絡(luò)序列轉(zhuǎn)主機序列
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
對于IP,首先我們要先轉(zhuǎn)成整數(shù),再要解決大小端問題。
系統(tǒng)給了直接能解決這兩個問題的接口:
#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);// 點分十進制字符串
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
這里的inet_addr
就是把一個點分十進制的字符串轉(zhuǎn)化成整數(shù)再進行大小端處理。
整體代碼:
void InitServer()
{
// 創(chuàng)建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
// 綁定IP與port
struct sockaddr_in si;
bzero(&si, sizeof si);
si.sin_family = AF_INET;// 協(xié)議家族
si.sin_port = htons(_port);// 端口號,注意大小端問題
si.sin_addr.s_addr = inet_addr(_ip.c_str());// ip
// 綁定
int n = bind(_sockfd, (struct sockaddr*)&si, sizeof si);
assert(n != -1);
}
2.3 啟動服務(wù)器
首先要知道服務(wù)器要死循環(huán),永遠不退出,除非用戶刪除。站在操作系統(tǒng)的角度,服務(wù)器是常駐內(nèi)存中的進程。
而我們啟動服務(wù)器的時候要傳遞進去IP和端口號。
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cout << "incorrect number of parameters" << std::endl;
exit(1);
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
std::unique_ptr<UDPServer> ptr(new UDPServer(port, ip));
return 0;
}
那么IP該怎么傳遞呢?
2.4 IP的綁定
這里的127.0.0.1叫做本地環(huán)回
它的作用就是用來做服務(wù)器代碼測試的,意思就是如果我們綁定的IP是127.0.0.1的話,在應(yīng)用層發(fā)送的消息不會進入物理層,也就不會發(fā)送出去。
當我們運行起來后想要查看網(wǎng)絡(luò)情況就可以用指令netstat
后邊也可以附帶參數(shù):
-a:顯示所有連線中的Socket;
-e:顯示網(wǎng)絡(luò)其他相關(guān)信息;
-i:顯示網(wǎng)絡(luò)界面信息表單;
-l:顯示監(jiān)控中的服務(wù)器的Socket;
-n:直接使用ip地址(數(shù)字),而不通過域名服務(wù)器;
-p:顯示正在使用Socket的程序識別碼和程序名稱;
-t:顯示TCP傳輸協(xié)議的連線狀況;
-u:顯示UDP傳輸協(xié)議的連線狀況;
那我們?nèi)绻胍W(wǎng)通信呢?該用什么IP呢?難道是云服務(wù)器上的公網(wǎng)IP嗎?
但是我們發(fā)現(xiàn)綁定不了。
因為云服務(wù)器是虛擬化服務(wù)器(不是真實的IP),不能直接綁定公網(wǎng)IP。
既然公網(wǎng)IP邦綁定不了,那么內(nèi)網(wǎng)IP(局域網(wǎng)IP)呢?
答案是可以,說明這個IP是屬于這個服務(wù)器的
但是這里不是一個內(nèi)網(wǎng)的就無法找到。
所以現(xiàn)在的問題是服務(wù)器啟動后怎么收到信息呢?(消息已經(jīng)發(fā)送到主機,現(xiàn)在要向上交付)
實際上,一款服務(wù)器不建議指明一個IP。因為可能服務(wù)器有很多IP,如果我們綁定了一個比如說IP1,那么其他進程發(fā)送給IP2服務(wù)器就收不到了。
這里的INADDR_ANY
實際上就是0,這樣綁定后,發(fā)送到這臺主機上所有的數(shù)據(jù),只要是訪問綁定的端口(8080)的,服務(wù)器都能收到。這樣就不會因為綁定了一個具體的IP而漏掉其他IP的信息
static const std::string defaultip = "0.0.0.0";// 默認IP
所以現(xiàn)在我們就不需要傳遞IP了。
2.5 讀取數(shù)據(jù)recvfrom
服務(wù)端要獲取到用戶端發(fā)送過來的數(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);
參數(shù)說明:
sockfd
:從哪個套接字讀。buf
:數(shù)據(jù)放入的緩沖區(qū)。len
:緩沖區(qū)長度。flags
:讀取方式。 0代表阻塞式讀取。src_addr
和addrlen
:輸出型參數(shù),返回對應(yīng)的消息內(nèi)容是從哪一個客戶端發(fā)出的。第一個是自己定義的結(jié)構(gòu)體,第二個是結(jié)構(gòu)體長度。
void start()
{
char buf[1024];
while(1)
{
struct sockaddr_in peer;
socklen_t len = sizeof peer;
sssize_t s = recvfrom(_sockfd, buf, sizeof buf - 1, 0, (struct sockaddr*)&peer, &len);
}
}
現(xiàn)在我們想要知道是誰發(fā)送過來的消息,信息都被保存到了peer結(jié)構(gòu)體中,我們知道IP信息在peer.sin_addr.s_addr
中,首先這是一個網(wǎng)絡(luò)序列,要轉(zhuǎn)成主機序列,其次為了方便觀察,要把它轉(zhuǎn)換成點分十進制。
而這兩個操作系統(tǒng)給了一個接口能夠解決:
char *inet_ntoa(struct in_addr in);
同樣獲取端口號的時候也要由網(wǎng)絡(luò)序列轉(zhuǎn)成主機序列:
uint16_t ntohs(uint16_t netshort);
整體代碼:
void start()
{
char buf[1024];
while(1)
{
struct sockaddr_in peer;
socklen_t len = sizeof peer;
ssize_t s = recvfrom(_sockfd, buf, sizeof buf - 1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
std::string cip = inet_ntoa(peer.sin_addr);
uint16_t cport = ntohs(peer.sin_port);
std::string msg = buf;
std::cout << " [" << cip << "@" << cport << " ]# " << msg << std::endl;
}
}
}
現(xiàn)在只需要等待用戶端發(fā)送數(shù)據(jù)即可。
三、用戶端實現(xiàn)
首先我們要發(fā)送數(shù)據(jù),就得知道客戶端的IP和port。
而這里的IP就必須指明。
uint16_t _serverport;
std::string _serverip;
int _sockfd;
這里的IP和port指的是要發(fā)送給誰。
創(chuàng)建套接字就跟前面的一樣:
// 創(chuàng)建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
3.1 綁定問題
這里的客戶端必須綁定IP和端口,來表示主機唯一性和進程唯一性。
但是不需要顯示的bind。
那么為什么前面服務(wù)端必須顯示的綁定port呢?
因為服務(wù)器的端口號是眾所周知的,不能改變,如果變了就找不到服務(wù)器了。
而客戶端只需要有就可以,只用標識唯一性即可。
舉個例子:
我們手機上有很多的app,而每個服務(wù)端是一家公司寫的,但是客戶端卻是多個公司寫的。如果我們綁定了特定的端口,萬一兩個公司都用了同一個端口號呢?這樣就直接沖突了。
所以操作系統(tǒng)會自動形成端口進行綁定。(在發(fā)送數(shù)據(jù)的時候自動綁定)
所以創(chuàng)建客戶端我們只用創(chuàng)建套接字即可。
void InitClient()
{
// 創(chuàng)建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
}
3.2 發(fā)送數(shù)據(jù)sendto
#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);
這里的參數(shù)和上面的recvfrom差不多,而這里的結(jié)構(gòu)體內(nèi)部我們要自己填充目的IP和目的端口號。文章來源:http://www.zghlxwxcb.cn/news/detail-433027.html
void start()
{
struct sockaddr_in si;
bzero(&si, sizeof(si));
si.sin_family = AF_INET;
si.sin_addr.s_addr = inet_addr(_serverip.c_str());
si.sin_port = htons(_serverport);
std::string msg;
while(1)
{
std::cout << "Please input: ";
std::cin >> msg;
sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&si, sizeof si);
}
}
當然這里是同一臺主機之間測試,如果是不同的機器,我們傳遞參數(shù)的時候就要傳遞公網(wǎng)IP,例如我們這臺云服務(wù)器的公網(wǎng)IP是:
我們在運行的時候:文章來源地址http://www.zghlxwxcb.cn/news/detail-433027.html
./UDPClient 43.143.106.44 8080
四、源碼
// UDPServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <strings.h>
#include <netinet/in.h>
#include <string.h>
#include <cassert>
#include <functional>
static const std::string defaultip = "0.0.0.0";// 默認IP
class UDPServer
{
public:
UDPServer(const uint16_t& port, const std::string ip = defaultip)
: _port(port)
, _ip(ip)
, _sockfd(-1)
{}
void InitServer()
{
// 創(chuàng)建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
// 綁定IP與port
struct sockaddr_in si;
bzero(&si, sizeof si);
si.sin_family = AF_INET;// 協(xié)議家族
si.sin_port = htons(_port);// 端口號,注意大小端問題
// si.sin_addr.s_addr = inet_addr(_ip.c_str());// ip
si.sin_addr.s_addr = INADDR_ANY;
// 綁定
int n = bind(_sockfd, (struct sockaddr*)&si, sizeof si);
assert(n != -1);
}
void start()
{
char buf[1024];
while(1)
{
struct sockaddr_in peer;
socklen_t len = sizeof peer;
ssize_t s = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
buf[s] = 0;// 結(jié)尾
std::string cip = inet_ntoa(peer.sin_addr);
uint16_t cport = ntohs(peer.sin_port);
std::string msg = buf;
std::cout << "[" << cip << "@" << cport << "]# " << msg << std::endl;
}
}
}
private:
uint16_t _port;
std::string _ip;
int _sockfd;
};
// UDPServer.cc
#include "UDPServer.hpp"
#include <memory>
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cout << "incorrect number of parameters" << std::endl;
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<UDPServer> ptr(new UDPServer(port));
ptr->InitServer();
ptr->start();
return 0;
}
// UDPClient.hpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <strings.h>
#include <netinet/in.h>
#include <string.h>
#include <cassert>
class UDPClient
{
public:
UDPClient(const std::string& serverip, const uint16_t& port)
: _serverip(serverip)
, _serverport(port)
, _sockfd(-1)
{}
void InitClient()
{
// 創(chuàng)建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
}
void start()
{
struct sockaddr_in si;
bzero(&si, sizeof(si));
si.sin_family = AF_INET;
si.sin_addr.s_addr = inet_addr(_serverip.c_str());
si.sin_port = htons(_serverport);
std::string msg;
while(1)
{
std::cout << "Please input: ";
std::cin >> msg;
sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&si, sizeof si);
}
}
private:
uint16_t _serverport;
std::string _serverip;
int _sockfd;
};
// UDPClient.cc
#include "UDPClient.hpp"
#include <memory>
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cout << "incorrect number of parameters" << std::endl;
exit(1);
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
std::unique_ptr<UDPClient> ptr(new UDPClient(ip, port));
ptr->InitClient();
ptr->start();
return 0;
}
到了這里,關(guān)于【網(wǎng)絡(luò)編程】demo版UDP網(wǎng)絡(luò)服務(wù)器實現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!