1.預備知識
1.1理解源IP地址和目的IP地址
上篇博客由唐僧的例子我們知道:
在IP數(shù)據(jù)包頭部中,有兩個IP地址,分別叫做源IP地址,和目的IP地址。
思考一下: 不考慮中間的一系列步驟,兩臺主機我們光有IP地址就可以完成通信了嘛? 想象一下發(fā)qq消息的例子,有了IP地址能夠把消息發(fā)送到對方的機器上。
但是我們把數(shù)據(jù)從主機A發(fā)生到主機B主機,是目的嗎?
并不是目的,是手段。
真正通信的不是這兩個機器!其實這是兩臺機器上面的軟件(人)!
數(shù)據(jù)有IP標識一臺唯一的主機,因此可以把一臺主機上的數(shù)據(jù)交給另外一臺主機。但是對方機器上不止一個程序,因此還需要有一個其他的標識來區(qū)分出,這個數(shù)據(jù)要給哪個程序進行解析。
那用誰來標識各自主機上客戶或者服務進程的唯一性呢?
為了更好的表示一臺主機上服務進程的唯一性,我們采用端口號port,標識服務器進程、客戶端的唯一性!
因此用進程的唯一性和主機IP的唯一性就可以保證兩臺主機的服務來進行直接通信。
1.2認識端口號
端口號(port)是傳輸層協(xié)議的內容
雖然端口號是傳輸層協(xié)議的內容,但是在應用層可以被調用,通過系統(tǒng)調用接口,向一個進程關聯(lián)上一個端口號。
- 端口號是一個2字節(jié)16位的整數(shù);
- 端口號用來標識一個進程, 告訴操作系統(tǒng), 當前的這個數(shù)據(jù)要交給哪一個進程來處理;
- IP地址 + 端口號能夠標識網(wǎng)絡上的某一臺主機的某一個進程;
- 一個端口號只能被一個進程占用
ip地址(主機全網(wǎng)唯一性)+該主機上的端口號,標識該服務器上進程的唯一性
ipA+portA ----->該主機上對應的服務進程,是全網(wǎng)中是唯一的一個進程!
ipB+portB ----->該主機上對應的服務進程,是全網(wǎng)中是唯一的一個進程!
網(wǎng)絡通信的本質:其實就是進程間的通信
進程之間通信的本質是什么呢?
- 需要讓不同的進程,先看到同一份資源 -----> 網(wǎng)絡
- 通信不就是在做IO嗎?----> 所以,我們所有上網(wǎng)的行為,無外乎就兩種:a.我要把我的數(shù)據(jù)發(fā)出去 b. 我要收到別人給我發(fā)的數(shù)據(jù)
現(xiàn)在還有一些問題:
1.客戶端和服務端上IP地址是不同的,但是它們兩個進程關聯(lián)的端口號可以一樣嗎?如客服端一個進程端口號是8080,服務端一個進程端口號是8080。
是可以的。
IP保證了全網(wǎng)唯一,port保證在主機內部的唯一性。
如果客戶端和服務端在一臺主機上,它們的端口號一定不能一樣!
2.進程已經有pid了,為什么要有port呢?
進程已經有pid可以保證它的唯一性看起來是可以的。但是實際上不行。
a. 系統(tǒng)是系統(tǒng),網(wǎng)絡是網(wǎng)絡,單獨設置 ---- 系統(tǒng)與網(wǎng)絡解耦
b. 需要客戶端每次都能找到服務器進程 ---- 服務器的唯一性不能做任何改變 (IP+port),尤其是端口不能隨意改變 —> 不能使用輕易會改變的值
c. 不是所有的進程都要提供網(wǎng)絡服務或者請求,但是所有的進程都需要pid
3.未來進程可以和端口號(port)關聯(lián)起來,我們就可以找到這臺主機上網(wǎng)絡服務進程
進程+port —> 網(wǎng)絡服務進程
那底層OS如何根據(jù)port找到指定的進程?
實際上每個進程在OS就是PCB數(shù)據(jù)結構,端口號類型是 uint16_t,說白了就是如何通過
uint16_t --> task_struct
實際上OS采用的是hashtable方案,OS內部維護了一張基于port作Key的一張哈希表,Value就是對應的PCB的地址。只要找到了對應port就找到了對應的進程PCB然后就可以數(shù)據(jù)交付給進程。
未來我們要學習網(wǎng)絡接口,網(wǎng)絡接口就是文件。 拿上來的數(shù)據(jù)找到這個進程,找到這個進程就能找到它的文件描述符表,根據(jù)文件描述符表就能找到文件對象,文件對象找到了它這個文件緩沖區(qū)就找到了,然后就可以把數(shù)據(jù)拷貝到它的緩沖區(qū)里,最后就相當于網(wǎng)絡數(shù)據(jù)放到了文件中,最后就如同讀文件一樣把數(shù)據(jù)讀上去了。
一個端口號只能被一個進程綁定,那一個進程可以綁定多個端口號嗎?
可以的。如10086的多個客服。
理解源端口號和目的端口號
傳輸層協(xié)議(TCP和UDP)的數(shù)據(jù)段中有兩個端口號, 分別叫做源端口號和目的端口號. 就是在描述 “數(shù)據(jù)是誰發(fā)的, 要發(fā)給誰”;
最后一個問題:
我們在網(wǎng)絡通信的過程中,IP+port標識唯一性,client->server,除了數(shù)據(jù),要把自己的ip和port發(fā)給對方嗎?
是的需要,我們還要發(fā)回來。未來發(fā)送數(shù)據(jù)的時候,一定會“多發(fā)”一部分數(shù)據(jù) —> 多發(fā)的數(shù)據(jù)以協(xié)議的形式呈現(xiàn)。
這里可能會有些問題,第一次怎么知道對方port的?
最開始的時候服務端的端口號是不變的,并且我們用的APP等根本不是我們寫的,寫服務和客戶端的是一家公司,客戶端在寫的時候它要請求的時候它客戶端內置的端口號已經內置好了。而且內置好之后是絕對不變的。
1.3認識TCP協(xié)議
這里我們先對TCP(Transmission Control Protocol 傳輸控制協(xié)議)有一個直觀的認識, 后面在學到TCP協(xié)議在細說
- 傳輸層協(xié)議
- 有連接
- 可靠傳輸
- 面向字節(jié)流
1.4認識UDP協(xié)議
此處我們也是對UDP(User Datagram Protocol 用戶數(shù)據(jù)報協(xié)議)有一個直觀的認識;,后面再詳細討論.
- 傳輸層協(xié)議
- 無連接
- 不可靠傳輸
- 面向數(shù)據(jù)報
并不是說可靠就是好的,不可靠就是不好的。可靠和不可靠其實是一個中性詞。
可靠是有成本的 – 往往比較復雜 --> 維護&&編碼
不可靠 – 比較簡單 – 維護&&使用
都有自己合適的應用場景。
1.5網(wǎng)絡字節(jié)序
我們已經知道,內存中的多字節(jié)數(shù)據(jù)相對于內存地址有大端和小端之分,磁盤文件中的多字節(jié)數(shù)據(jù)相對于文件中的偏移地址也有大端小端之分,網(wǎng)絡數(shù)據(jù)流同樣有大端小端之分, 那么如何定義網(wǎng)絡數(shù)據(jù)流的地址呢?
大端是數(shù)據(jù)低地址存在內存高地址,數(shù)據(jù)高地址存內存低地址
小端是數(shù)據(jù)低地址存在內存低地址,數(shù)據(jù)高地址存內存高地址
如0x12345678 高<—低
大端機器內存放的是 12 34 56 78 低—>高
小端機器內存放的是 78 56 34 12 低—>高
如果是一個大端機把數(shù)據(jù)通過網(wǎng)絡轉給小端機,小端機把這個收到大端機的數(shù)據(jù)按照小端機存,就可能在這個服務器把數(shù)據(jù)解釋反了?,F(xiàn)在問題是作為接收方你怎么知道你接收的數(shù)據(jù)是大端還是小端?
也就是說不管是大端機還是小端機規(guī)定發(fā)到網(wǎng)絡中的數(shù)據(jù)都必須是大端!
- 發(fā)送主機通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內存地址從低到高的順序發(fā)出;
- 接收主機把從網(wǎng)絡上接到的字節(jié)依次保存在接收緩沖區(qū)中,也是按內存地址從低到高的順序保存;
- 因此,網(wǎng)絡數(shù)據(jù)流的地址應這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出的數(shù)據(jù)是高地址.
- TCP/IP協(xié)議規(guī)定,網(wǎng)絡數(shù)據(jù)流應采用大端字節(jié)序,即低地址高字節(jié).
- 不管這臺主機是大端機還是小端機, 都會按照這個TCP/IP規(guī)定的網(wǎng)絡字節(jié)序來發(fā)送/接收數(shù)據(jù);如果當前發(fā)送主機是小端, 就需要先將數(shù)據(jù)轉成大端; 否則就忽略, 直接發(fā)送即可
為使網(wǎng)絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯后都能正常運行,可以調用以下庫函數(shù)做網(wǎng)絡字節(jié)序和主機字節(jié)序的轉換
- 這些函數(shù)名很好記,h表示host,n表示network,l表示32位長整數(shù),s表示16位短整數(shù)。
- 例如htonl表示將32位的長整數(shù)從主機字節(jié)序轉換為網(wǎng)絡字節(jié)序,例如將IP地址轉換后準備發(fā)送。
- 如果主機是小端字節(jié)序,這些函數(shù)將參數(shù)做相應的大小端轉換然后返回。
- 如果主機是大端字節(jié)序,這些 函數(shù)不做轉換,將參數(shù)原封不動地返回。
2.socket編程接口
上面我們所說 ip+port ----->該主機上對應的服務進程,是全網(wǎng)中是唯一的一個進程!
ip+port就是套接字,socket
socket 常見API
// 創(chuàng)建 socket 文件描述符 (TCP/UDP, 客戶端 + 服務器)
int socket(int domain, int type, int protocol);
// 綁定端口號 (TCP/UDP, 服務器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 開始監(jiān)聽socket (TCP, 服務器)
int listen(int socket, int backlog);
// 接收請求 (TCP, 服務器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立連接 (TCP, 客戶端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockaddr結構
我們可以看到上面struct sockaddr *addr出現(xiàn)次數(shù)挺多的。實際上在網(wǎng)絡上通信的時候套接字種類是比較多的,下面是常見的三種:
1.網(wǎng)絡套接字
2.原始套接字
3.unix域間套接字
網(wǎng)絡套接字:運用于網(wǎng)絡跨主機之間通信+本地通信
unix域間套接字: 本地通信
我們現(xiàn)在在使用網(wǎng)絡編程通信時是應用層調傳輸層的接口,而原始套接字:跳過傳輸層訪問其他層協(xié)議中的有效數(shù)據(jù)。主要用于抓包,偵測網(wǎng)絡情況。。
我們現(xiàn)在知道套接字種類很多,它們應用的場景也是不一樣的。所以未來要完成這三種通信就需要有三套不同接口,但是思想上用的都是套接字的思想。因此接口設計者不想設計三套接口,只想設計一套接口,可以通過不通的參數(shù),解決所有網(wǎng)絡或者其他場景下的通信網(wǎng)絡。
struct sockaddr_in :適用于網(wǎng)絡通信
struct sockaddr_un:適用于域間通信
前兩個字節(jié)都是16位地址類型:代表采用的是網(wǎng)絡通信還是本地通信
未來接口里面雖然是struct sockaddr *addr,但是你要填充的是要不是struct sockaddr_in,要不是struct sockaddr_un。然后把其他一個強制類型轉換傳給struct sockaddr *addr。然后在內部根據(jù)struct sockaddr *addr的前兩個字節(jié)判斷傳過來的是struct sockaddr_in還是struct sockaddr_un,然后在做強制類型轉換轉成對應的結構。
這里就有點像C++,父類和子類多態(tài)的意思。
sockaddr 結構
sockaddr_in 結構
這個結構里主要有三部分信息從上到下: 地址類型(協(xié)議家族), 端口號, IP地址.
in_addr結構
in_addr用來表示一個IPv4的IP地址. 其實就是一個32位的整數(shù)
3.UDP網(wǎng)絡程序
3.1UDP Server服務器端
#pragma once
#include<iostream>
#include<string>
using namespace std;
//這個只能寫在類外,類中static const只能定義整數(shù)
static const string defaultIP="0.0.0.0";
class udpServer
{
public:
udpServer(const uint16_t& port,const string& ip=defaultIP)
:_port(port),_ip(ip),_sockfd(-1)
{}
//初始化服務器
void initServer()
{}
//啟動服務器
void start()
{}
~udpServer()
{}
private:
//寫服務器要給它ip和port
string _ip;//服務器IP,先寫成這樣后面再說
uint16_t _port;//服務器端口號port
int _sockfd;// socket返回值時介紹
};
進行網(wǎng)絡通信首先要創(chuàng)建套接字
套接字創(chuàng)建一套網(wǎng)絡通信的文件機制(在文件系統(tǒng)層面,把對應的網(wǎng)卡文件打開),其實是在底層幫我打開一個文件,把文件和網(wǎng)卡設備關聯(lián)起來。
domain (域):代表未來這個套接字是用來網(wǎng)絡通信還是本地通信
type:套接字提供的服務類型
protocol:未來想使用什么協(xié)議如TCP、UDP。一般默認為0,因為前兩個參數(shù)就已經幫第三個參數(shù)確定采用什么協(xié)議了。
成功時返回一個文件描述符,失敗時返回-1,錯誤碼被設置。Linux下一切皆文件,返回文件描述符我們知道未來所有接口大概率都跟這個值有關,通過這個文件描述符向文件中讀,向文件中寫。
enum //錯誤碼類型
{
SOCKET_ERR=2,
};
void initServer()
{
//1.創(chuàng)建socket
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd == -1)
{
cerr<<"Server fail: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
//套接字文件創(chuàng)建好了只有一個文件描述符,前面說ip+port-->socket,因此需要bind綁定
//將我們字節(jié)設置的ip和port設置到操作系統(tǒng)中。告訴操作系統(tǒng)ip和port給那個文件用的。
//2.bind,將ip和port和套接字文件進行綁定
}
sockfd:調用socket返回的文件描述符
struct sockaddr:
今天我們寫的是網(wǎng)絡通信,因此需要一個struct sockaddr_in結構。里面最重要的字段有三個,
第一個是16位地址類型也叫做協(xié)議家族 AF_INET
第二個是這個服務器要綁定的16位端口號是誰
第三個是這個服務器要綁定的32位ip地址是誰
addrlen:未來要傳的結構體的長度
成功返回0,失敗返回-1
void initServer()
{
//1.創(chuàng)建socket
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd == -1)
{
cerr<<"Server fail: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
//2.bind
struct sockaddr_in local;
}
先看看這個結構
關于ip地址:
192.168.80.30 -----> 點分十進制的風格的IP,字符串,可讀性好
但是實際上我們知道一個IP地址,這里分成4個字節(jié),每個字節(jié)取值0-255,就可以用一個uint32_t 類型4個字節(jié)就能標識 -----> 整數(shù)類型的ip,網(wǎng)絡通信使用
那現(xiàn)在就需要上面轉換成下面,下面轉換成上面。那該怎么轉呢?
這個并不用我們轉,庫已經幫我們寫好了,用庫的就行。
不過原理我們可以看一下
字符串轉uint_32 原理類型,把字符串分成4部分,每部分字符串一次設進p1、p2、p3、p4里,然后再把整個結構體強制類型轉換賦值給uint32_t
結構體剩下的就是填充,是為了照顧結構體內存對齊的。
void initServer()
{
//1.創(chuàng)建socket
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd == -1)
{
cerr<<"Server fail: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
//2.bind,綁定ip+port
struct sockaddr_in local;//定義了一個變量,在棧上,用戶層
bzero(&local,sizeof(local));//結構體內容初始設置為0
local.sin_family=AF_INET;//采用網(wǎng)絡通信, AF_INET填充一個struct sockaddr_in結構體,為了用于網(wǎng)絡通信
local.sin_port=htons(_port);//轉成網(wǎng)絡字節(jié)序, 給別人發(fā)消息,也要把自己的port和ip發(fā)送給對方
}
void initServer()
{
//1.創(chuàng)建socket
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd == -1)
{
cerr<<"Server fail: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
//2.bind,綁定ip+port
struct sockaddr_in local;//定義了一個變量,在棧上,用戶層
bzero(&local,sizeof(local));//結構體內容初始設置為0
local.sin_family=AF_INET;//采用網(wǎng)絡通信, AF_INET填充一個struct sockaddr_in結構體,為了用于網(wǎng)絡通信
local.sin_port=htons(_port);//轉成網(wǎng)絡字節(jié)序, 給別人發(fā)消息,也要把自己的port和ip發(fā)送給對方
local.sin_addr.s_addr=inet_addr(_ip.c_str());//1.string->uint32_t 2.htonl
}
inet_addr
1.將點分十進制ip地址 字符串轉整數(shù)
2.將這個整數(shù)轉成網(wǎng)絡字節(jié)序
現(xiàn)在終于把結構體填好了,但是現(xiàn)在OS并不知道你設置的ip和port,因為這是在用戶棧上定義的,因此需要調用bind進行綁定
enum
{
SOCKET_ERR=2,
BIND_ERR
};
void initServer()
{
//1.創(chuàng)建socket
_sockfd=socket(AF_INET,SOCK_DGRAM,0);//這里 AF_INET,是創(chuàng)建一個網(wǎng)絡通信套接字
if(_sockfd == -1)
{
cerr<<"Server fail: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
//2.bind,綁定ip+port
struct sockaddr_in local;//定義了一個變量,在棧上,用戶層
bzero(&local,sizeof(local));//結構體內容初始設置為0
local.sin_family=AF_INET;//采用網(wǎng)絡通信, AF_INET填充一個struct sockaddr_in結構體,為了用于網(wǎng)絡通信
local.sin_port=htons(_port);//轉成網(wǎng)絡字節(jié)序, 給別人發(fā)消息,也要把自己的port和ip發(fā)送給對方
local.sin_addr.s_addr=inet_addr(_ip.c_str());//1.string->uint32_t 2.htonl
int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n == -1)
{
cerr<<"bind fail:"<<strerror(errno)<<endl;
exit(BIND_ERR);
}
}
自此UDP服務器預備工作完成。 1.創(chuàng)建socket 2.bind綁定
服務器初始化,要綁定ip和port,因此需要我們自己傳,所以我們使用命令行參數(shù)
void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_ip local_port\n\n";
}
// ./udpServer ip port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[2]);
string ip=argv[1];
unique_ptr<udpServer> ups(new udpServer(ip,port));
ups->initServer();
ups->start();
return 0;
}
start先寫一個空的死循環(huán),現(xiàn)在這個服務器也可以啟動了。
下面說一說這個自己填寫的ip地址的問題
127.0.0.1 本地環(huán)回
未來可以使用這個地址做服務器代碼測試
目前我們寫的服務器在應用層,如果綁定的ip地址是127.0.0.1 ,當我做測試的時候未來發(fā)信息和讀消息其實都是在本主機內,數(shù)據(jù)貫穿協(xié)議棧之后再進行流動不會到底物理層
服務器跑起來如何看到呢?
netstat 查看當前網(wǎng)絡情況
netstat -nuap n:能顯示數(shù)字顯示數(shù)字 u:UDP a:all p:進程
但是我們想這個服務器未來在全網(wǎng)服務,因此我們需要用到公網(wǎng)ip,但是云服務器都是虛擬化的服務器,不能直接bind你的公網(wǎng)ip。虛擬機或者真實的Linux環(huán)境,你可以bind你的公網(wǎng)ip。
那不能綁定公網(wǎng)ip,如何讓別人能找到我呢?
實際情況下一款網(wǎng)絡服務器不建議指明一個ip。
就是說未來服務器不要顯示綁定一個ip,因為有時候一些原因服務器上不止一個ip,如果今天綁定了一個特定ip,大家可以用的是ip1,ip2等都在向這個端口號為8080的服務器發(fā)送消息,但此時只綁定一個明確的ip,那么最終只能收到目的ip就是自己顯示綁定的ip發(fā)送的數(shù)據(jù)。別人用其他ip向8080發(fā)送數(shù)據(jù)那就不能收到了。
所以在給struct sockaddr_in填充ip地址時,一般寫法如下。這也是為什么在構造的時候給ip缺省值0.0.0.0的原因。 任意地址綁定!未來發(fā)送到這臺機器上的所有的數(shù)據(jù)只要訪問的端口號port是8080,都可以交付給這個服務器。
void initServer()
{
//1.創(chuàng)建socket
_sockfd=socket(AF_INET,SOCK_DGRAM,0);//這里 AF_INET,是創(chuàng)建一個網(wǎng)絡通信套接字
if(_sockfd == -1)
{
cerr<<"Server fail: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
//2.bind,綁定ip+port
struct sockaddr_in local;//定義了一個變量,在棧上,用戶層
bzero(&local,sizeof(local));//結構體內容初始設置為0
local.sin_family=AF_INET;//采用網(wǎng)絡通信, AF_INET填充一個struct sockaddr_in結構體,為了用于網(wǎng)絡通信
local.sin_port=htons(_port);//轉成網(wǎng)絡字節(jié)序, 給別人發(fā)消息,也要把自己的port和ip發(fā)送給對方
local.sin_addr.s_addr=inet_addr(_ip.c_str());//1.string->uint32_t 2.htonl
//local.sin_addr.s_addr=htonl(INADDR_ANY);//任意地址bind,服務器的真實寫法
int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n == -1)
{
cerr<<"bind fail:"<<strerror(errno)<<endl;
exit(BIND_ERR);
}
}
既可以寫成注釋掉的那種寫法,也可以用自己寫缺省值是0.0.0.0的ip,然后在構造的時候就不需要給ip傳值了。這兩種寫法任意選擇。
// ./udpServer port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<udpServer> ups(new udpServer(port));
ups->initServer();
ups->start();
return 0;
}
這也是上面查看網(wǎng)絡信息的那個圖片,端口號是8080,ip地址是0.0.0.0的原因。
現(xiàn)在把啟動服務器start寫一下
服務器讀取數(shù)據(jù)
sockfd:從那個套接字讀
buf:讀上來的數(shù)據(jù)放那個緩沖區(qū)
len:這個緩沖區(qū)多大
flags:怎么讀,阻塞式的讀?。?)
src_addr:輸出型參數(shù),今天讀過來數(shù)據(jù)想知道是誰發(fā)的。返回對應的消息內容,是從哪一個client發(fā)來的。
addrlen:輸出型參數(shù),傳過去的結構體多大。
因為是網(wǎng)絡通信,因此傳struct sockaddr_in結構體對象過去,會把client的ip和port消息填入這個結構體中。
成功時返回讀取到字節(jié)的個數(shù),失敗返回-1
void start()
{
//服務器的本質其實就是一個死循環(huán) ---> 常駐內存的進程
char buffer[gunm];
for(;;)
{
//讀數(shù)據(jù)
struct sockaddr_in peer;
socklen_t len=sizeof(peer);//必填
ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
//1. 數(shù)據(jù)是什么 2.誰發(fā)的
if(s > 0)
{
buffer[s]=0;
string clientip=inet_ntoa(peer.sin_addr);//1.網(wǎng)絡字節(jié)序 2.int->點分十進制
uint16_t clientport=ntohs(peer.sin_port);//網(wǎng)絡字節(jié)序轉主機
string str=buffer;
cout<<clientip<<"["<<clientport<<"]# "<<str<<endl;
}
}
}
1.字節(jié)序轉變 2.int->點分十進制
udp服務器完整代碼
#pragma once
#include<iostream>
#include<string>
#include<stdlib.h>
#include<string.h>
#include<cerrno>
#include<functional>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
static const string defaultIP="0.0.0.0";
const int gunm=1024;
enum
{
USAGE_ERR,
SOCKET_ERR=2,
BIND_ERR
};
class udpServer
{
typedef function<void(int,string,uint16_t,string)> func_t;
public:
udpServer(const func_t& func,const uint16_t& port,const string& ip=defaultIP)
:_callback(func),_port(port),_ip(ip),_sockfd(-1)
{}
void initServer()
{
//1.創(chuàng)建socket
_sockfd=socket(AF_INET,SOCK_DGRAM,0);//這里 AF_INET,是創(chuàng)建一個網(wǎng)絡通信套接字
if(_sockfd == -1)
{
cerr<<"Server fail: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
//2.bind,綁定ip+port
struct sockaddr_in local;//定義了一個變量,在棧上,用戶層
bzero(&local,sizeof(local));//結構體內容初始設置為0
local.sin_family=AF_INET;//采用網(wǎng)絡通信, AF_INET填充一個struct sockaddr_in結構體,為了用于網(wǎng)絡通信
local.sin_port=htons(_port);//轉成網(wǎng)絡字節(jié)序, 給別人發(fā)消息,也要把自己的port和ip發(fā)送給對方
local.sin_addr.s_addr=inet_addr(_ip.c_str());//1.string->uint32_t 2.htonl
//local.sin_addr.s_addr=htonl(INADDR_ANY);//任意地址bind,服務器的真實寫法
int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n == -1)
{
cerr<<"bind fail:"<<strerror(errno)<<endl;
exit(BIND_ERR);
}
}
void start()
{
//服務器的本質其實就是一個死循環(huán) ---> 常駐內存的進程
char buffer[gunm];
for(;;)
{
//讀數(shù)據(jù)
struct sockaddr_in peer;
socklen_t len=sizeof(peer);//必填
ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
//1. 數(shù)據(jù)是什么 2.誰發(fā)的
if(s > 0)
{
buffer[s]=0;
string clientip=inet_ntoa(peer.sin_addr);//1.網(wǎng)絡字節(jié)序 2.int->點分十進制
uint16_t clientport=ntohs(peer.sin_port);
string str=buffer;
cout<<clientip<<"["<<clientport<<"]# "<<str<<endl;
}
}
}
~udpServer()
{}
private:
string _ip;
uint16_t _port;
int _sockfd;
};
#include "udpServer.hpp"
#include <memory>
void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
// ./udpServer port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<udpServer> ups(new udpServer(port));
ups->initServer();
ups->start();
return 0;
}
3.2UDP Client客戶端
客戶端和服務端用到的接口幾乎差不多,因此這里直接就包含這些頭文件。
#pragma once
#include<iostream>
#include<string>
#include<stdlib.h>
#include<string.h>
#include<cerrno>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<pthread.h>
using namespace std;
class udpClient
{
public:
udpClient(const string& ip,const uint16_t& port)
:_serverip(ip),_serverport(port),_sockfd(-1)
{}
void initClient()
{
}
void run()
{
}
~udpClient()
{}
private:
string _serverip;
uint16_t _serverport;
int _sockfd;
};
#include"udpClient.hpp"
#include<memory>
void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " server_ip server_port\n\n";
}
//./udpClient ip port
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
}
string ip=argv[1];
uint16_t port=atoi(argv[2]);
unique_ptr<udpClient> ups(new udpClient(ip,port));
ups->initClient();
ups->run();
return 0;
}
客戶端這里必須要知道服務端ip和port,然后才知道要往那個服務器發(fā)。發(fā)送到對應服務器后,只不過服務器內部在使用的時候不在看ip了,只看端口,把數(shù)據(jù)發(fā)給綁定這個端口的進程就好了。
下面初始化客服端
void initClient()
{
//1.創(chuàng)建socket
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd == -1)
{
cerr<<"Client fail: "<<strerror(errno)<<endl;
exit(1);
}
cout << "socket success: " << " : " << _sockfd << endl;
}
剛才服務端 1.創(chuàng)建socket 2.bind,現(xiàn)在問題就來了
client要不要bind?client要不要顯示的bind(需不需要程序員自己bind)?
所謂bind是讓套接字文件信息和網(wǎng)絡ip和port產生關聯(lián)。未來通信客戶端和服務端都有自己的ip和port。那client要不要bind?
client必須要bind,但不需要顯示bind(不需要程序員自己bind)
那server服務端為什么一定要顯示bind?
在服務端這里bind綁定的時候,最重要的根本就不是綁定ip,最重要的而是綁定port。未來服務器要明確的port,不能隨意改變。所有必須顯示bind某個端口。只要服務器啟動成功,一定是bind成功,它對應的端口號一定是屬于它自己的,另外這個端口號是眾所周知的不會輕易改變。所以需要用戶顯示bind。
客戶端只要有port就可以,它的port是多少不重要!具有唯一性才重要!未來當客戶端發(fā)信息把自己端口號填上,然后服務端能收到,然后給返回來就可以了。所以客戶端不需要顯示綁定。
那client客戶端為什么不用顯示bind?
寫服務器的是一家公司,寫client是無數(shù)家公司。
比如寫抖音App是字節(jié)跳動一家公司,但你的手機一定裝滿各自APP,手機裝了這么多客戶端,如果每個客戶端都自己說就要綁定9090這個端口號,那一定是誰先啟動那個App先拿到這個端口號,那其他的客戶端就啟動不起來了。
所以客戶端不需要明確哪一個,只需要有就可以了,保證唯一性就行了。并且這由OS自動形成端口進行bind,然后還會綁定ip。
OS在什么時候,如何bind。
如何bind,OS發(fā)現(xiàn)bind沒綁就采用隨機策略形成一個端口號,然后使用bind方法進行綁定。
在首次向服務器sendto數(shù)據(jù)時,OS發(fā)現(xiàn)沒有bind綁定ip和port,只寫了服務器的ip和port,所以OS會自動綁定ip和port。
客戶端 1.創(chuàng)建socket
下面啟動客服端
發(fā)送消息使用sendto接口
sockfd:往那個套接字發(fā)送
buf:發(fā)送的內容是什么
len:內容多長
flags:發(fā)送方式 ,阻塞式發(fā)送(0)有數(shù)據(jù)就發(fā)沒數(shù)據(jù)就等
dest_addr:輸入型參數(shù),告訴客戶端要發(fā)給誰。
給個struct sockaddr_in結構體,往結構體填充要發(fā)給服務器的ip地址和port。
addrlen:輸入型參數(shù),這個結構體多大
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 str;
while(1)
{
//發(fā)
cout<<"Please Enter# ";
string str;
getline(cin,str);
sendto(_sockfd,str.c_str(),str.size(),0,(struct sockaddr*)&server,sizeof(server));
}
}
客戶端完整代碼
#pragma once
#include<iostream>
#include<string>
#include<stdlib.h>
#include<string.h>
#include<cerrno>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<pthread.h>
using namespace std;
class udpClient
{
public:
udpClient(const string& ip,const uint16_t& port)
:_serverip(ip),_serverport(port),_sockfd(-1)
{}
void initClient()
{
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd == -1)
{
cerr<<"Client fail: "<<strerror(errno)<<endl;
exit(1);
}
cout << "socket success: " << " : " << _sockfd << endl;
}
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 str;
while(1)
{
cout<<"Please Enter# ";
string str;
getline(cin,str);
sendto(_sockfd,str.c_str(),str.size(),0,(struct sockaddr*)&server,sizeof(server));
}
}
~udpClient()
{}
private:
string _serverip;
uint16_t _serverport;
int _sockfd;
};
4.根據(jù)UDP客戶端服務端做的設計
服務器把數(shù)據(jù)讀上來就完了嗎?并不是,它可能還會對這些數(shù)據(jù)進行處理。
因此我們添加一個回調函數(shù),對數(shù)據(jù)進行處理。
#pragma once
#include<iostream>
#include<string>
#include<stdlib.h>
#include<string.h>
#include<cerrno>
#include<functional>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
static const string defaultIP="0.0.0.0";
const int gunm=1024;
enum
{
USAGE_ERR,
SOCKET_ERR,
BIND_ERR
};
class udpServer
{
//包裝器
typedef function<void(int,string,uint16_t,string)> func_t;
public:
udpServer(const func_t& func,const uint16_t& port,const string& ip=defaultIP)
:_callback(func),_port(port),_ip(ip),_sockfd(-1)
{}
void initServer()
{
//1.創(chuàng)建socket
_sockfd=socket(AF_INET,SOCK_DGRAM,0);//這里 AF_INET,是創(chuàng)建一個網(wǎng)絡通信套接字
if(_sockfd == -1)
{
cerr<<"Server fail: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
//2.bind,綁定ip+port
struct sockaddr_in local;//定義了一個變量,在棧上,用戶層
bzero(&local,sizeof(local));//結構體內容初始設置為0
local.sin_family=AF_INET;//采用網(wǎng)絡通信, AF_INET填充一個struct sockaddr_in結構體,為了用于網(wǎng)絡通信
local.sin_port=htons(_port);//轉成網(wǎng)絡字節(jié)序, 給別人發(fā)消息,也要把自己的port和ip發(fā)送給對方
local.sin_addr.s_addr=inet_addr(_ip.c_str());//1.string->uint32_t 2.htonl
//local.sin_addr.s_addr=htonl(INADDR_ANY);//任意地址bind,服務器的真實寫法
int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n == -1)
{
cerr<<"bind fail:"<<strerror(errno)<<endl;
exit(BIND_ERR);
}
}
void start()
{
//服務器的本質其實就是一個死循環(huán) ---> 常駐內存的進程
char buffer[gunm];
for(;;)
{
//讀數(shù)據(jù)
struct sockaddr_in peer;
socklen_t len=sizeof(peer);//必填
ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
//1. 數(shù)據(jù)是什么 2.誰發(fā)的
if(s > 0)
{
buffer[s]=0;
string clientip=inet_ntoa(peer.sin_addr);//1.網(wǎng)絡字節(jié)序 2.int->點分十進制
uint16_t clientport=ntohs(peer.sin_port);
string str=buffer;
cout<<clientip<<"["<<clientport<<"]# "<<str<<endl;
//并不是收到數(shù)據(jù)打印就完了,還要就行業(yè)務處理
_callback(_sockfd,clientip,clientport,str);
}
}
}
~udpServer()
{}
private:
string _ip;
uint16_t _port;
int _sockfd;
func_t _callback;//回調函數(shù)
};
就可以在handerMessage里對message進行特定的業(yè)務處理,而不關心message怎么來的。從而實現(xiàn)server通信和業(yè)務邏輯解耦!
#include "udpServer.hpp"
#include <memory>
void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
void handerMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
}
// ./udpServer port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<udpServer> ups(new udpServer(handerMessage,port));
ups->initServer();
ups->start();
return 0;
}
注意云服務器的網(wǎng)絡端口默認都是基本關閉的!需要你自己打開。不然別人客戶端根據(jù)這個ip和port也找不到這個服務器。
4.1字典+熱加載
字典就是做中文和英文直接的翻譯,因此需要一個一對一映射關系,所以我們選擇unordered_map容器做為字典。
首先要給字典初始化,在容器中插入一些中文與英文的映射關系。因此我們可以自己創(chuàng)建一個dict.txt文件,然后從文件中讀,這里可以采用C++關于對文件的操作,ifstream用起來很方便。while這里的判斷在C++的IO流說過,這里不再細說。
const string filename = "dict.txt";
unordered_map<string, string> dict;
void initDict()
{
//讀
ifstream iss(filename);
string str, Key, Val;
while (iss >> str)
{
// 分割字符串
//傳入的是輸出型參數(shù)
if (CurString(str, &Key, &Val, ":"))
{
dict.insert(make_pair(Key, Val));
}
}
cout << "load dict success" << endl;
}
把每次讀過來的字符串做分割,這里我們以 " : " 作為分隔符,把分割好的Key,Val插入到容器中。
bool CurString(string &str, string *s1, string *s2, string step)
{
auto pos = str.find(step.c_str());
if (pos != string::npos)
{
*s1 = str.substr(0, pos); //[)前閉后開
*s2 = str.substr(pos + 1); //[)前閉后開
return true;
}
else
{
return false;
}
}
初始化詞典完成之后,就可以進行業(yè)務處理了
// demo1 翻譯
void handerMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
string renopose_str;
auto it = dict.find(message);
if (it != dict.end())
{
renopose_str += it->second;
}
else
{
renopose_str = "UnKnow";
}
// 返回
struct sockaddr_in client;
bzero(&client, sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr = inet_addr(clientip.c_str());
client.sin_port = htons(clientport);
sendto(sockfd, renopose_str.c_str(), renopose_str.size(), 0, (struct sockaddr *)&client, sizeof(client));
}
完整代碼
#include "udpServer.hpp"
#include <memory>
#include <unordered_map>
#include <fstream>
void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
const string filename = "dict.txt";
unordered_map<string, string> dict;
bool CurString(string &str, string *s1, string *s2, string step)
{
auto pos = str.find(step.c_str());
if (pos != string::npos)
{
*s1 = str.substr(0, pos); //[)
*s2 = str.substr(pos + 1); //[)
return true;
}
else
{
return false;
}
}
void initDict()
{
ifstream iss(filename);
string str, Key, Val;
while (iss >> str)
{
// 分割字符串
if (CurString(str, &Key, &Val, ":"))
{
dict.insert(make_pair(Key, Val));
}
}
cout << "load dict success" << endl;
}
// demo1 翻譯
void handerMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
string renopose_str;
auto it = dict.find(message);
if (it != dict.end())
{
renopose_str += it->second;
}
else
{
renopose_str = "UnKnow";
}
// 返回
struct sockaddr_in client;
bzero(&client, sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr = inet_addr(clientip.c_str());
client.sin_port = htons(clientport);
sendto(sockfd, renopose_str.c_str(), renopose_str.size(), 0, (struct sockaddr *)&client, sizeof(client));
}
現(xiàn)在服務器把翻譯返回給客戶端了,那客戶端也得能接收讀取。
客戶端這里我們只需要改變一個地方就可以了
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 str;
while(1)
{
//demo1
//發(fā)
cout<<"Please Enter# ";
cin>>str;
sendto(_sockfd,str.c_str(),str.size(),0,(struct sockaddr*)&server,sizeof(server));
//收
char buffer[1024];
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(n>0)
{
buffer[n]=0;
cout<<"Server provide translate: "<<buffer<<endl;
}
}
}
這樣簡單的一個字典就寫好了,還有一個熱加載是什么意思呢?
對于目前這個字典來說,熱加載的意思是不需要重啟服務器,就能使在文件中新增的內容立即生效。
具體可以這樣做,可以對某個信號進行捕捉設置一個捕捉方法,然后再里面調用字典初始化函數(shù)。這樣就完成了熱加載。
void reload(int signo)
{
(void)signo;
initDict();
}
// ./udpServer port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
// demo1 翻譯
//熱加載
signal(3,reload);//對3號信號進行捕捉
initDict();
unique_ptr<udpServer> ups(new udpServer(handerMessage,port));
ups->initServer();
ups->start();
return 0;
}
4.2shell命令行
現(xiàn)在做的是,想在客戶端輸入一些指令
ls -a -l
pwd
touch text.txt
...
希望服務端能在自己的服務器上執(zhí)行成功并且把結構返回給客戶端。
我們這里寫的是簡單的,有些命令不能執(zhí)行。
現(xiàn)在自己想改這份代碼怎么改呢?不要這份翻譯了。
是不是只需要再寫一個完成這樣任務的業(yè)務邏輯就好了,這就是解耦的好處!
這里我們需要用popen接口,這樣就不需要我們像以前實現(xiàn)myshell那樣,創(chuàng)建子進程然后在子進程中調用exec*系列的函數(shù)執(zhí)行程序替換,那樣麻煩了。
popen相當于做了pipe+fork+exec*的工作
command:未來要執(zhí)行的命令字符串
返回類型FILE * :通過管道以文件的方式把對應的執(zhí)行結果寫到文件中
type:對這個文件以什么方式打開 “w”、“r”等等
失敗返回NULL,要不是fork創(chuàng)建子進程失敗,要不pipe創(chuàng)建管道失敗,要不內存申請失敗
// demo2 命令
void execCommand(int sockfd, string clientip, uint16_t clientport, string cmd)
{
//1.cmd解析 ls -a -l
//2.如果必要可能需要 fork,exec*
//對于一些執(zhí)行不執(zhí)行
if (cmd.find("rm") != string::npos || cmd.find("mv") != string::npos || cmd.find("rmdir") != string::npos)
{
cerr << clientip << ":" << clientport << " 正在做一個非法的操作: " << cmd << endl;
return;
}
string response;
FILE *pf = popen(cmd.c_str(), "r");//以讀的方式打開文件
if (pf == NULL)
response = cmd + "excel fail";
char line[1024];
while (fgets(line, sizeof(line) - 1, pf))//每次從文件中讀一行
{
response += line;
}
pclose(pf);
//發(fā)
struct sockaddr_in client;
bzero(&client, sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr = inet_addr(clientip.c_str());
client.sin_port = htons(clientport);
sendto(sockfd, response.c_str(), response.size(), 0, (struct sockaddr *)&client, sizeof(client));
}
// ./udpServer port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
// demo2 命令
unique_ptr<udpServer> ups(new udpServer(execCommand, port));
ups->initServer();
ups->start();
return 0;
}
客戶端也還是改那個地方就好了
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 str;
while(1)
{
//demo2
//發(fā)
cout<<"[wdl@VM-28-3-centos 24test_3_16]$ ";
string str;
getline(cin,str);
sendto(_sockfd,str.c_str(),str.size(),0,(struct sockaddr*)&server,sizeof(server));
//收
char buffer[1024];
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
/ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(n>0)
{
buffer[n]=0;
cout<<buffer<<endl;
}
}
}
4.3聊天室
下面我們寫一個能群聊的客戶端服務器??蛻舳税l(fā)來一個消息想讓服務器做一個轉發(fā),讓所有在線的人都能收到這個消息,然后自己也能接收到別人的消息。
正常來說我們的服務器應該寫一個用戶注冊登錄功能,但是這里不想搞那么麻煩。
這里這樣做,如果客戶端發(fā) “online” 就加入群聊,然后發(fā)的消息就由服務器推送給群在線的所有人也可以就收別人發(fā)的消息,客戶端發(fā) “offline” 退出群聊。
因此我們首先寫一個用戶管理的模塊 onlineUser.hpp
#pragma once
#include<iostream>
#include<string>
#include<unordered_map>
using namespace std;
//用戶管理
class User
{
public:
User(const string& ip,const uint16_t& port)
:_ip(ip),_port(port)
{}
~User()
{}
string& Getip()
{
return _ip;
}
uint16_t& Getport()
{
return _port;
}
private:
string _ip;
uint16_t _port;
};
//在線用戶管理
class onlineUser
{
public:
onlineUser()
{}
void addUser(const string& ip,const uint16_t& port)
{
string id=ip+"#"+to_string(port);
usp.insert(make_pair(id,User(ip,port)));
}
void eraseUser(const string& ip,const uint16_t& port)
{
string id=ip+"#"+to_string(port);
usp.erase(id);
}
bool isOnline(const string& ip,const uint16_t& port)
{
string id=ip+"#"+to_string(port);
return usp.find(id) != usp.end() ? true:false;
}
//給每一個在線用戶轉發(fā)某個客戶端發(fā)的消息
void boradcast(int sockfd,const string& ip,const uint16_t& port,const string& msg)
{
for(auto& us:usp)
{
struct sockaddr_in client;
memset(&client,0,sizeof(client));
client.sin_family=AF_INET;
client.sin_addr.s_addr=inet_addr(us.second.Getip().c_str());
client.sin_port=htons(us.second.Getport());
string s=ip+"-"+to_string(port)+"# ";//用來標識是那臺主機發(fā)的
s+=msg;//發(fā)的信息
//轉發(fā)
sendto(sockfd,s.c_str(),s.size(),0,(struct sockaddr*)&client,sizeof(client));
}
}
~onlineUser()
{}
private:
unordered_map<string,User> usp;
};
static onlineUser onlineuser;
//demo3 聊天室
void routeMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
if(message == "online") onlineuser.addUser(clientip,clientport);
if(message == "offline") onlineuser.eraseUser(clientip,clientport);
if(onlineuser.isOnline(clientip,clientport))
{
onlineuser.boradcast(sockfd,clientip,clientport,message);
}
else
{
struct sockaddr_in client;
bzero(&client, sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr = inet_addr(clientip.c_str());
client.sin_port = htons(clientport);
string s="你還沒有上線,請先上線,運行: online";
sendto(sockfd, s.c_str(), s.size(), 0, (struct sockaddr *)&client, sizeof(client));
}
}
// ./udpServer port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
//demo3 聊天室
unique_ptr<udpServer> ups(new udpServer(routeMessage, port));
ups->initServer();
ups->start();
return 0;
}
客戶端這里我們做一些設計,可能你發(fā)一條消息之后不在發(fā)了,但是還在群聊里,別人發(fā)的信息我也應該能收到,也不能把發(fā)和讀放在一塊,因為它們都是阻塞式等待。所以這里我們寫一個線程。一個線程讀,一個線程寫。
class udpClient
{
public:
//...
//讀線程
static void* readMessage(void* args)
{
int sockfd=*(static_cast<int*>(args));
while(true)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
ssize_t n=recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(n>=0)
{
buffer[n]=0;
cout<<buffer<<endl;
}
}
return nullptr;
}
void run()
{
//demo3 讀
pthread_create(&_pid,nullptr,readMessage,(void*)&_sockfd);
pthread_detach(_pid);
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 str;
while(1)
{
//demo3
//發(fā)
cerr<<"Enter# ";//
string str;
getline(cin,str);
sendto(_sockfd,str.c_str(),str.size(),0,(struct sockaddr*)&server,sizeof(server));
}
}
private:
string _serverip;
uint16_t _serverport;
int _sockfd;
pthread_t _pid;//線程ID
};
客戶端這里還有一個問題,因為現(xiàn)在這里我們窗口就一個,你發(fā)送的消息和接收的消息就會揉在一起看起來比較亂。因此我們創(chuàng)建一個管道文件。把客戶端收到的消息打印的時候都重定向到這個管道文件中,然后我們在開一個端口從管道文件中讀,這樣就把發(fā)送和接收也分開了。就不會揉在一起了。
所以在發(fā)的時候我們這樣寫,cerr<<"Enter# "; 我們知道編譯器默認會打開三個文件,標準輸入,標準輸出,標準錯誤,我們這里是把標準輸出重定向到管道文件,但標準錯誤并沒有改變,因此使用cerr,可以在用戶發(fā)信息的時候看到這個提示。
5.windows客戶端與linux服務端交匯
windows環(huán)境下實現(xiàn)客戶端和我們在linux上寫服務端使用的socket套接字接口一模一樣,但是有三處不一樣的地方
windows環(huán)境下要進行套接字方面的編程要需要使用庫的,在安裝vs的時候就已經有了。因此首先要包含頭文件
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")//windows socket 2_版本 32位 lib庫
其次要對WinSocket初始化
WSAStartup啟動WinSocket,MAKEWORD構建一個2.2庫的版本,把構建出來的結果放到wsd中。這里就有點像你的客戶端有版本,自己寫的版本在啟動的時候windows要和導入的庫的版本進行對比。
int main()
{
WSAData wsd;//初始化信息
//啟動Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {/*進行WinSocket的初始化,
windows 初始化socket網(wǎng)絡庫,申請2, 2的版本,windows socket編程必須先初始化。*/
cout << "WSAStartup Error = " << WSAGetLastError() << endl;
return 0;
}
else {
cout << "WSAStartup Success" << endl;
}
//...
}
最后關閉socket并清理Winsock
int main()
{
//...
closesocket(sockfd);
WSACleanup();
return 0;
}
剩下的在linux怎么寫就在windows怎么寫
客戶端
#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable:4996) //vs會報一個錯誤,這里是屏蔽掉這個錯誤
#include<iostream>
#include<string>
#include<cstring>
#include<thread>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
//服務器ip,端口
const string serverip = "124.223.54.148";
uint16_t serverport = 8080;
int main()
{
WSAData wsd;//初始化信息
//啟動Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {/*進行WinSocket的初始化,
windows 初始化socket網(wǎng)絡庫,申請2, 2的版本,windows socket編程必須先初始化。*/
cout << "WSAStartup Error = " << WSAGetLastError() << endl;
return 0;
}
else {
cout << "WSAStartup Success" << endl;
}
//1.創(chuàng)建套接字
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == SOCKET_ERROR) {
cout << "socket Error = " << WSAGetLastError() << endl;
return 1;
}
else {
cout << "socket Success" << endl;
}
//創(chuàng)建收消息的線程
thread t1([&]()
{
char buffer[1024];
while (true)
{
struct sockaddr_in peer;
int len = sizeof(peer);
int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if (n > 0)
{
buffer[n] = 0;
cout << "server reponose: " << buffer << endl;
}
else break;
}
}
);
//發(fā)
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 msg;
while (true)
{
cout << "Please Enter# ";
getline(cin, msg);
int n=sendto(sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&server, sizeof(peer));
if (n < 0)
{
cerr << "sendto error!" << endl;
break;
}
}
//清理
closesocket(sockfd);
WSACleanup();
return 0;
}
服務端
udpServer.hpp一點沒變
udpServer.cc變了一點點
#include "udpServer.hpp"
#include <memory>
#include <unordered_map>
#include <fstream>
#include <signal.h>
#include <stdio.h>
void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
void handerMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
string renopose_str;
// 返回
struct sockaddr_in client;
bzero(&client, sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr = inet_addr(clientip.c_str());
client.sin_port = htons(clientport);
sendto(sockfd, renopose_str.c_str(), renopose_str.size(), 0, (struct sockaddr *)&client, sizeof(client));
}
// ./udpServer port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
}
uint16_t port = atoi(argv[1]);
// string ip=argv[1];
unique_ptr<udpServer> ups(new udpServer(handerMessage, port));
ups->initServer();
ups->start();
return 0;
}
多平臺涉及到編碼方式不一樣,但是我們這里是簡單實現(xiàn)的,沒有考慮這個問題,因此不要發(fā)中文。文章來源:http://www.zghlxwxcb.cn/news/detail-843415.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-843415.html
到了這里,關于【Linux】網(wǎng)絡編程套接字一的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!