北京開發(fā)區(qū)建設(shè)委員會網(wǎng)站資格網(wǎng)絡(luò)營銷軟件大全
文章目錄
- 一、引入
- 二、服務(wù)端實(shí)現(xiàn)
- 2.1 創(chuàng)建套接字socket
- 2.2 綁定bind
- 2.3 啟動服務(wù)器
- 2.4 IP的綁定
- 2.5 讀取數(shù)據(jù)recvfrom
- 三、用戶端實(shí)現(xiàn)
- 3.1 綁定問題
- 3.2 發(fā)送數(shù)據(jù)sendto
- 四、源碼
一、引入
在上一章【網(wǎng)絡(luò)編程】socket套接字中我們講述了TCP/UDP協(xié)議,這一篇就是簡單實(shí)現(xiàn)一個UDP協(xié)議的網(wǎng)絡(luò)服務(wù)器。
我們也講過其實(shí)網(wǎng)絡(luò)通信的本質(zhì)就是進(jìn)程間通信。而進(jìn)程間通信無非就是讀和寫(IO)。
所以現(xiàn)在我們就要寫一個服務(wù)端(server)接收數(shù)據(jù),客戶端(client)發(fā)送數(shù)據(jù)。
二、服務(wù)端實(shí)現(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
:一個域,標(biāo)識了這個套接字的通信類型(網(wǎng)絡(luò)或者本地)。
只用關(guān)注上面兩個類,第一個AF_UNIX
表示本地通信,而AF_INET
表示網(wǎng)絡(luò)通信。
type
:套接字提供服務(wù)的類型。
這一章我們講的式UDP,所以使用SOCK_DGRAM
。
protocol
:想使用的協(xié)議,默認(rèn)為0即可,因?yàn)榍懊娴膬蓚€參數(shù)決定了,就已經(jīng)決定了是TCP還是UDP協(xié)議了。
返回值:
成功則返回打開的文件描述符(指向網(wǎng)卡文件),失敗返回-1。
而從這里我們就聯(lián)想到系統(tǒng)中的文件操作,未來各種操作都要通過這個文件描述符,所以在服務(wù)端類中還需要一個成員變量表示文件描述符。
static const std::string defaultip = "0.0.0.0";// 默認(rèn)IPclass 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套接字有詳細(xì)介紹)。
address_len
:傳入結(jié)構(gòu)體的長度。
所以我們要先定義一個sockaddr_in
結(jié)構(gòu)體填充數(shù)據(jù),在傳遞進(jìn)去。
struct sockaddr_in {short int sin_family; // 地址族,一般為AF_INET或PF_INETunsigned 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>
// 主機(jī)序列轉(zhuǎn)網(wǎng)絡(luò)序列
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
// 網(wǎng)絡(luò)序列轉(zhuǎn)主機(jī)序列
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);// 點(diǎn)分十進(jìn)制字符串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
就是把一個點(diǎn)分十進(jìn)制的字符串轉(zhuǎn)化成整數(shù)再進(jìn)行大小端處理。
整體代碼:
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與portstruct 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),永遠(yuǎn)不退出,除非用戶刪除。站在操作系統(tǒng)的角度,服務(wù)器是常駐內(nèi)存中的進(jìn)程。
而我們啟動服務(wù)器的時候要傳遞進(jìn)去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ā)送的消息不會進(jìn)入物理層,也就不會發(fā)送出去。
當(dāng)我們運(yùn)行起來后想要查看網(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)綁定不了。
因?yàn)樵品?wù)器是虛擬化服務(wù)器(不是真實(shí)的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ā)送到主機(jī),現(xiàn)在要向上交付)
實(shí)際上,一款服務(wù)器不建議指明一個IP。因?yàn)榭赡芊?wù)器有很多IP,如果我們綁定了一個比如說IP1,那么其他進(jìn)程發(fā)送給IP2服務(wù)器就收不到了。
這里的INADDR_ANY
實(shí)際上就是0,這樣綁定后,發(fā)送到這臺主機(jī)上所有的數(shù)據(jù),只要是訪問綁定的端口(8080)的,服務(wù)器都能收到。這樣就不會因?yàn)榻壎艘粋€具體的IP而漏掉其他IP的信息
static const std::string defaultip = "0.0.0.0";// 默認(rèn)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)成主機(jī)序列,其次為了方便觀察,要把它轉(zhuǎn)換成點(diǎn)分十進(jìn)制。
而這兩個操作系統(tǒng)給了一個接口能夠解決:
char *inet_ntoa(struct in_addr in);
同樣獲取端口號的時候也要由網(wǎng)絡(luò)序列轉(zhuǎn)成主機(jī)序列:
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ù)即可。
三、用戶端實(shí)現(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和端口,來表示主機(jī)唯一性和進(jìn)程唯一性。
但是不需要顯示的bind。
那么為什么前面服務(wù)端必須顯示的綁定port呢?
因?yàn)榉?wù)器的端口號是眾所周知的,不能改變,如果變了就找不到服務(wù)器了。
而客戶端只需要有就可以,只用標(biāo)識唯一性即可。
舉個例子:
我們手機(jī)上有很多的app,而每個服務(wù)端是一家公司寫的,但是客戶端卻是多個公司寫的。如果我們綁定了特定的端口,萬一兩個公司都用了同一個端口號呢?這樣就直接沖突了。
所以操作系統(tǒng)會自動形成端口進(jìn)行綁定。(在發(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和目的端口號。
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);}
}
當(dāng)然這里是同一臺主機(jī)之間測試,如果是不同的機(jī)器,我們傳遞參數(shù)的時候就要傳遞公網(wǎng)IP,例如我們這臺云服務(wù)器的公網(wǎng)IP是:
我們在運(yùn)行的時候:
./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";// 默認(rèn)IPclass 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與portstruct 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());// ipsi.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;
}