一、網(wǎng)絡(luò)編程中的一些基礎(chǔ)知識(shí)
1、認(rèn)識(shí)端口號(hào)
在前面我們說(shuō)過(guò)可以使用IP地址來(lái)標(biāo)識(shí)一臺(tái)主機(jī),但是我們光有IP地址就可以完成通信了嘛?
答案是:不可以,當(dāng)我們的主機(jī)接收到了數(shù)據(jù)以后還要確定這個(gè)數(shù)據(jù)是發(fā)送給哪一個(gè)進(jìn)程的,兩臺(tái)主機(jī)的兩個(gè)軟件進(jìn)行網(wǎng)絡(luò)通信時(shí),我們還需要有一個(gè)其他的標(biāo)識(shí)來(lái)區(qū)分出這個(gè)數(shù)據(jù)要給哪個(gè)程序進(jìn)行解析,于是就有了端口號(hào)。
端口號(hào)(port)是傳輸層協(xié)議的內(nèi)容,它有以下特點(diǎn):
- 端口號(hào)是一個(gè)
2
字節(jié)16
位的整數(shù)。 - 端口號(hào)用來(lái)標(biāo)識(shí)一個(gè)進(jìn)程, 告訴操作系統(tǒng)當(dāng)前的這個(gè)數(shù)據(jù)要交給哪一個(gè)進(jìn)程來(lái)處理。
- IP地址 + 端口號(hào)能夠標(biāo)識(shí)網(wǎng)絡(luò)上的某一臺(tái)主機(jī)的某一個(gè)進(jìn)程。
- 一個(gè)進(jìn)程可以綁定多個(gè)端口號(hào),但是一個(gè)端口號(hào)不能被多個(gè)進(jìn)程綁定。
理解 “端口號(hào)” 和 “進(jìn)程ID”
我們之前在學(xué)習(xí)系統(tǒng)編程的時(shí)候, 學(xué)習(xí)了 pid
表示唯一一個(gè)進(jìn)程; 此處我們的端口號(hào)也是唯一表示一個(gè)進(jìn)程. 那么這兩者之間是怎樣的關(guān)系? 那在進(jìn)行網(wǎng)絡(luò)通信時(shí)為什么不直接用PID來(lái)代替port呢?
進(jìn)程ID(PID)是用來(lái)標(biāo)識(shí)系統(tǒng)內(nèi)所有進(jìn)程的唯一性的,它是屬于系統(tǒng)級(jí)的概念;而端口號(hào)(port)是用來(lái)標(biāo)識(shí)需要對(duì)外進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)請(qǐng)求的進(jìn)程的唯一性的,它是屬于網(wǎng)絡(luò)的概念。
一臺(tái)機(jī)器上可能會(huì)有大量的進(jìn)程,但并不是所有的進(jìn)程都要進(jìn)行網(wǎng)絡(luò)通信,可能有很大一部分的進(jìn)程是不需要進(jìn)行網(wǎng)絡(luò)通信的本地進(jìn)程,此時(shí)PID雖然也可以標(biāo)識(shí)這些網(wǎng)絡(luò)進(jìn)程的唯一性,但在該場(chǎng)景下就不太合適了,而且如果用PID代替端口號(hào),會(huì)導(dǎo)致網(wǎng)絡(luò)管理模塊與進(jìn)程管理模塊產(chǎn)生耦合關(guān)系,不利于設(shè)計(jì)出高內(nèi)聚低耦合的軟件。
所以在網(wǎng)絡(luò)通信中我們可以使用:IP地址+Port號(hào) 標(biāo)識(shí)互聯(lián)網(wǎng)中唯一的一個(gè)進(jìn)程。
此外,從上面通信的例子我們能看出網(wǎng)絡(luò)通信的本質(zhì):其實(shí)是進(jìn)程間通信!,位于不同主機(jī)中的兩個(gè)進(jìn)程通過(guò)網(wǎng)絡(luò)進(jìn)行了進(jìn)程間通信。
2、認(rèn)識(shí)TCP協(xié)議和UDP協(xié)議
傳輸層協(xié)議(TCP和UDP)的數(shù)據(jù)段中有兩個(gè)端口號(hào),分別叫做源端口號(hào)和目的端口號(hào)。 描述的是 “數(shù)據(jù)是誰(shuí)發(fā)的, 要發(fā)給誰(shuí)”。
認(rèn)識(shí)TCP協(xié)議
此處我們先對(duì)TCP(Transmission Control Protocol 傳輸控制協(xié)議)有一個(gè)直觀的認(rèn)識(shí); 后面我們?cè)僭敿?xì)討論TCP的一些細(xì)節(jié)問(wèn)題。
- 傳輸層協(xié)議
- 有連接,TCP協(xié)議是面向連接的,如果兩臺(tái)主機(jī)之間想要進(jìn)行數(shù)據(jù)傳輸,那么必須要先建立連接,當(dāng)連接建立成功后才能進(jìn)行數(shù)據(jù)傳輸。
- 可靠傳輸,TCP協(xié)議是保證可靠的協(xié)議,數(shù)據(jù)在傳輸過(guò)程中如果出現(xiàn)了丟包、亂序等情況,TCP協(xié)議都有對(duì)應(yīng)的解決方法。
- 面向字節(jié)流
認(rèn)識(shí)UDP協(xié)議
此處我們也是對(duì)UDP(User Datagram Protocol 用戶數(shù)據(jù)報(bào)協(xié)議)有一個(gè)直觀的認(rèn)識(shí),后面再詳細(xì)討論。
- 傳輸層協(xié)議
- 無(wú)連接,無(wú)需建立連接就可以進(jìn)行網(wǎng)絡(luò)傳輸
- 不可靠傳輸,無(wú)連接也就意味著UDP協(xié)議是不可靠的,數(shù)據(jù)在傳輸過(guò)程中如果出現(xiàn)了丟包、亂序等情況,是沒(méi)有辦法進(jìn)行處理的。
- 面向數(shù)據(jù)報(bào)
既然UDP協(xié)議是不可靠的,那為什么還要有UDP協(xié)議的存在?
首先,要保證數(shù)據(jù)傳輸?shù)目煽啃允切枰覀冏龈嗟墓ぷ鞯?,TCP協(xié)議雖然是一種可靠的傳輸協(xié)議,但這一定意味著TCP協(xié)議在底層需要做更多的工作,因此TCP協(xié)議底層的實(shí)現(xiàn)是比較復(fù)雜的。
同樣的,UDP協(xié)議雖然是一種不可靠的傳輸協(xié)議,但這一定意味著UDP協(xié)議在底層不需要做過(guò)多的工作,因此UDP協(xié)議底層的實(shí)現(xiàn)一定比TCP協(xié)議要簡(jiǎn)單,UDP協(xié)議雖然不可靠,但是它能夠快速的將數(shù)據(jù)發(fā)送給對(duì)方。
編寫(xiě)網(wǎng)絡(luò)通信代碼時(shí)具體采用TCP協(xié)議還是UDP協(xié)議,完全取決于上層的應(yīng)用場(chǎng)景。如果應(yīng)用場(chǎng)景嚴(yán)格要求數(shù)據(jù)在傳輸過(guò)程中的可靠性,此時(shí)我們就必須采用TCP協(xié)議,如果應(yīng)用場(chǎng)景允許數(shù)據(jù)在傳輸出現(xiàn)少量丟包,那么我們肯定優(yōu)先選擇UDP協(xié)議,因?yàn)閁DP協(xié)議足夠簡(jiǎn)單。
ps: 一些優(yōu)秀的網(wǎng)站在設(shè)計(jì)網(wǎng)絡(luò)通信算法時(shí),會(huì)同時(shí)采用TCP協(xié)議和UDP協(xié)議,當(dāng)網(wǎng)絡(luò)流暢時(shí)就使用UDP協(xié)議進(jìn)行數(shù)據(jù)傳輸,而當(dāng)網(wǎng)絡(luò)信號(hào)差時(shí)就使用TCP協(xié)議進(jìn)行數(shù)據(jù)傳輸,這樣既保證了數(shù)據(jù)的可靠性又保障了傳輸?shù)乃俾省?/p>
3、網(wǎng)絡(luò)字節(jié)序
計(jì)算機(jī)在存儲(chǔ)數(shù)據(jù)時(shí)是有大小端的概念的:
- 大端模式: 數(shù)據(jù)的高字節(jié)內(nèi)容保存在內(nèi)存的低地址處。
- 小端模式: 數(shù)據(jù)的高字節(jié)內(nèi)容保存在內(nèi)存的高地址處。
如果我們編寫(xiě)的程序只在本地機(jī)器上運(yùn)行,那么是不需要考慮大小端問(wèn)題的,因?yàn)橥慌_(tái)機(jī)器上的數(shù)據(jù)采用的存儲(chǔ)方式都是一樣的,要么采用的都是大端存儲(chǔ)模式,要么采用的都是小端存儲(chǔ)模式。但如果涉及網(wǎng)絡(luò)通信,那就必須考慮大小端的問(wèn)題,否則對(duì)端主機(jī)識(shí)別出來(lái)的數(shù)據(jù)可能與發(fā)送端想要發(fā)送的數(shù)據(jù)是不一致的,那么如何定義網(wǎng)絡(luò)數(shù)據(jù)流的地址呢?
- TCP/IP協(xié)議規(guī)定,網(wǎng)絡(luò)數(shù)據(jù)流應(yīng)采用大端字節(jié)序,即低地址高字節(jié)。
- 發(fā)送主機(jī)通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序發(fā)出。
- 接收主機(jī)把從網(wǎng)絡(luò)上接到的字節(jié)依次保存在接收緩沖區(qū)中,也是按內(nèi)存地址從低到高的順序保存。
- 因此,網(wǎng)絡(luò)數(shù)據(jù)流的地址應(yīng)這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出的數(shù)據(jù)是高地址.
- 不管這臺(tái)主機(jī)是大端機(jī)還是小端機(jī), 都會(huì)按照這個(gè)TCP/IP規(guī)定的網(wǎng)絡(luò)字節(jié)序來(lái)發(fā)送/接收數(shù)據(jù)。
- 如果當(dāng)前發(fā)送主機(jī)是小端,就需要先將數(shù)據(jù)轉(zhuǎn)成大端,否則就忽略直接發(fā)送即可;
需要注意的是,所有的大小端的轉(zhuǎn)化工作是由操作系統(tǒng)來(lái)完成的,因?yàn)樵摬僮鲗儆谕ㄐ偶?xì)節(jié),不過(guò)也有部分的信息需要我們自行進(jìn)行處理,比如端口號(hào)和IP地址。
為使網(wǎng)絡(luò)程序具有可移植性,使同樣的C代碼在大端和小端計(jì)算機(jī)上編譯后都能正常運(yùn)行,可以調(diào)用以下庫(kù)函數(shù)做網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序的轉(zhuǎn)換。
這些函數(shù)名很好記,h表示host,n表示network,l表示32位長(zhǎng)整數(shù),s表示16位短整數(shù)。
- 如果主機(jī)是小端字節(jié)序,這些函數(shù)將參數(shù)做相應(yīng)的大小端轉(zhuǎn)換然后返回;
- 如果主機(jī)是大端字節(jié)序,這些 函數(shù)不做轉(zhuǎn)換,將參數(shù)原封不動(dòng)地返回。
例如htonl
表示將32位的長(zhǎng)整數(shù)從主機(jī)字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序,例如將IP地址轉(zhuǎn)換后準(zhǔn)備發(fā)送。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
二、socket編程
socket 是“套接字”的意思,學(xué)習(xí) socket 編程,也就是學(xué)習(xí)計(jì)算機(jī)之間如何通信,并用編程語(yǔ)言來(lái)實(shí)現(xiàn)它。
socket API是一層抽象的網(wǎng)絡(luò)編程接口,適用于各種底層網(wǎng)絡(luò)協(xié)議,如IPv4、IPv6、UNIX Domain Socket。然而各種網(wǎng)絡(luò)協(xié)議的地址格式并不相同。
1、sockaddr結(jié)構(gòu)
套接字不僅支持跨網(wǎng)絡(luò)的進(jìn)程間通信,還支持本地的進(jìn)程間通信(域間套接字)。在進(jìn)行跨網(wǎng)絡(luò)通信時(shí)我們需要傳遞的端口號(hào)和IP地址,而本地通信則不需要,因此套接字提供了sockaddr_in
結(jié)構(gòu)體和sockaddr_un
結(jié)構(gòu)體,其中sockaddr_in
結(jié)構(gòu)體是用于跨網(wǎng)絡(luò)通信的,而sockaddr_un
結(jié)構(gòu)體是用于本地通信的。
為了讓套接字的網(wǎng)絡(luò)通信和本地通信能夠使用同一套函數(shù)接口,于是就出現(xiàn)了sockaddr
結(jié)構(gòu)體,該結(jié)構(gòu)體與sockaddr_in
和sockaddr_un
的結(jié)構(gòu)都不相同,但這三個(gè)結(jié)構(gòu)體頭部的16個(gè)比特位都是一樣的,這個(gè)字段叫做協(xié)議家族。
此時(shí)當(dāng)我們?cè)趥鬟f在傳參時(shí),就不用傳入sockeaddr_in *
或sockeaddr_un *
這樣的結(jié)構(gòu)體,而統(tǒng)一傳入sockeaddr *
這樣的結(jié)構(gòu)體。在設(shè)置參數(shù)時(shí)就可以通過(guò)設(shè)置協(xié)議家族這個(gè)字段,來(lái)表明我們是要進(jìn)行網(wǎng)絡(luò)通信還是本地通信,在這些API內(nèi)部就可以提取sockeaddr
結(jié)構(gòu)頭部的16位進(jìn)行識(shí)別,進(jìn)而得出我們是要進(jìn)行網(wǎng)絡(luò)通信還是本地通信,然后執(zhí)行對(duì)應(yīng)的操作。此時(shí)我們就通過(guò)通用sockaddr
結(jié)構(gòu),將套接字網(wǎng)絡(luò)通信和本地通信的參數(shù)類型進(jìn)行了統(tǒng)一。
sockaddr結(jié)構(gòu)體
sockaddr_in 結(jié)構(gòu)體
- IPv4和IPv6的地址格式定義在
netinet/in.h
中,IPv4地址用sockaddr_in
結(jié)構(gòu)體表示,包括16位地址類型,16位端口號(hào)和32位IP地址。 - IPv4、IPv6地址類型分別定義為常數(shù)
AF_INET、AF_INET6
。這樣,只要取得某種sockaddr
結(jié)構(gòu)體的首地址,不需要知道具體是哪種類型的sockaddr
結(jié)構(gòu)體,就可以根據(jù)地址類型字段確定結(jié)構(gòu)體中的內(nèi)容。 -
socket API
可以都用struct sockaddr*
類型表示,在使用的時(shí)候需要強(qiáng)制轉(zhuǎn)化成sockaddr_in
;這樣的好處是程序的通用性,可以接收IPv4、IPv6,以及UNIX Domain Socket各種類型的sockaddr
結(jié)構(gòu)體指針做為參數(shù)。
2、簡(jiǎn)單的UDP網(wǎng)絡(luò)程序
Ⅰ、服務(wù)器的創(chuàng)建
UDP服務(wù)器的初始化就只需要創(chuàng)建套接字和綁定就行了
創(chuàng)建套接字
// 創(chuàng)建 socket 文件描述符
int socket(int domain, int type, int protocol);
功能:socket
函數(shù)可以打開(kāi)一個(gè)網(wǎng)絡(luò)文件,用于網(wǎng)絡(luò)數(shù)據(jù)的通信。
對(duì)于一般的普通文件來(lái)說(shuō),當(dāng)用戶通過(guò)文件描述符將數(shù)據(jù)寫(xiě)到文件緩沖區(qū),然后再把數(shù)據(jù)刷到磁盤上就完成了數(shù)據(jù)的寫(xiě)入操作。
而對(duì)于現(xiàn)在socket
函數(shù)打開(kāi)的“網(wǎng)絡(luò)文件”來(lái)說(shuō),當(dāng)用戶將數(shù)據(jù)寫(xiě)到文件緩沖區(qū)后,操作系統(tǒng)會(huì)定期將數(shù)據(jù)刷到網(wǎng)卡里面,而網(wǎng)卡則是負(fù)責(zé)數(shù)據(jù)發(fā)送的,因此數(shù)據(jù)最終就發(fā)送到了網(wǎng)絡(luò)當(dāng)中。
參數(shù)說(shuō)明:
-
domain
:創(chuàng)建套接字的域(協(xié)議家族),也就是創(chuàng)建套接字的類型。該參數(shù)就相當(dāng)于struct sockaddr
結(jié)構(gòu)的前16個(gè)位。如果是本地通信就設(shè)置為AF_UNIX
,如果是網(wǎng)絡(luò)通信就設(shè)置為AF_INET
(IPv4)或AF_INET6
(IPv6)。 -
type
:創(chuàng)建套接字時(shí)所需的服務(wù)類型。如果是基于UDP的網(wǎng)絡(luò)通信,我們采用的就是SOCK_DGRAM
,叫做用戶數(shù)據(jù)報(bào)服務(wù),如果是基于TCP的網(wǎng)絡(luò)通信,我們采用的就是SOCK_STREAM
,叫做流式套接字,提供的是流式服務(wù)。 -
protocol
:創(chuàng)建套接字的協(xié)議類別。你可以指明為TCP或UDP,但該字段一般直接設(shè)置為0就可以了,設(shè)置為0表示的就是默認(rèn),此時(shí)會(huì)根據(jù)傳入的前兩個(gè)參數(shù)自動(dòng)推導(dǎo)出你最終需要使用的是哪種協(xié)議。
返回值說(shuō)明:
- 套接字創(chuàng)建成功返回一個(gè)文件描述符,創(chuàng)建失敗返回
-1
,同時(shí)錯(cuò)誤碼會(huì)被設(shè)置。
示例代碼:
// udp_server.hpp
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
class UdpServer
{
public:
UdpServer()
{}
void UdpServerInit()
{
// 1. 創(chuàng)建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
std::cerr << "create socket fail : " << strerror(errno) << std::endl;
exit(1);
}
std::cout << "create socket success! " << "sockfd : " << _sockfd << std::endl;
}
~UdpServer()
{
if (_sockfd > 0)
{
close(_sockfd);
}
}
private:
int _sockfd; // 套接字的文件描述符
};
// udp_server.cpp
#include "udp_server.hpp"
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<UdpServer> up(new UdpServer());
up->UdpServerInit();
return 0;
}
綁定函數(shù)
將程序的端口號(hào),IP地址等數(shù)據(jù)設(shè)置進(jìn)入操作系統(tǒng)內(nèi)核中
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
參數(shù)說(shuō)明:
-
sockfd
:要綁定的文件的文件描述符。也就是我們創(chuàng)建套接字時(shí)獲取到的文件描述符。 -
addr
:網(wǎng)絡(luò)相關(guān)的屬性信息,包括協(xié)議家族、IP地址、端口號(hào)等。 -
addrlen
:傳入的addr
結(jié)構(gòu)體的長(zhǎng)度。
返回值說(shuō)明:
- 綁定成功返回0,綁定失敗返回-1,同時(shí)錯(cuò)誤碼會(huì)被設(shè)置。
將點(diǎn)分10進(jìn)制的ip轉(zhuǎn)換為整數(shù)
in_addr_t inet_addr(const char *cp);
功能:該函數(shù)可以將主機(jī)序列的字符串風(fēng)格類型的IP, 轉(zhuǎn)換成為網(wǎng)絡(luò)序列中的整數(shù)風(fēng)格的IP地址。
將整數(shù)轉(zhuǎn)換為點(diǎn)分10進(jìn)制的ip
char *inet_ntoa(struct in_addr in);
功能: 該函數(shù)可以將網(wǎng)絡(luò)序列中的整數(shù)風(fēng)格的IP地址,轉(zhuǎn)換成為主機(jī)序列的字符串風(fēng)格類型的數(shù)據(jù)。
ps : 這兩個(gè)函數(shù)調(diào)用完畢以后不需要再進(jìn)行網(wǎng)絡(luò)序列與主機(jī)序列的轉(zhuǎn)化了。
套接字創(chuàng)建完畢后我們就需要進(jìn)行綁定了,但在綁定之前我們需要先定義一個(gè)struct sockaddr_in
結(jié)構(gòu),將對(duì)應(yīng)的網(wǎng)絡(luò)屬性信息填充到該結(jié)構(gòu)當(dāng)中,然后通過(guò)bind
函數(shù)設(shè)置進(jìn)入操作系統(tǒng)內(nèi)核當(dāng)中,由于該結(jié)構(gòu)體當(dāng)中還有部分選填字段,因此我們最好在填充之前對(duì)該結(jié)構(gòu)體變量里面的內(nèi)容進(jìn)行清空,然后再將協(xié)議家族、端口號(hào)、IP地址等信息填充到該結(jié)構(gòu)體變量當(dāng)中。
需要注意的是,在發(fā)送到網(wǎng)絡(luò)之前需要將端口號(hào)和IP轉(zhuǎn)換為網(wǎng)絡(luò)序列,由于端口號(hào)是16位的,因此我們需要使用前面說(shuō)到的htons
函數(shù)將端口號(hào)轉(zhuǎn)為網(wǎng)絡(luò)序列。此外,由于網(wǎng)絡(luò)當(dāng)中傳輸?shù)氖钦麛?shù)IP,我們需要調(diào)用inet_addr
函數(shù)將字符串IP轉(zhuǎn)換成整數(shù)IP。
當(dāng)網(wǎng)絡(luò)屬性信息填充完畢后,由于bind
函數(shù)提供的是通用參數(shù)類型,因此在傳入結(jié)構(gòu)體地址時(shí)還需要將struct sockaddr_in*
強(qiáng)轉(zhuǎn)為struct sockaddr*
類型后再進(jìn)行傳入。
// udp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
enum { SOCKET_ERR = 1, BIND_ERR};
// 默認(rèn)端口號(hào)
const static uint16_t default_port = 8080;
class UdpServer
{
public:
UdpServer(std::string ip, uint16_t port = default_port)
:_port(port), _ip(ip)
{
std::cout << "ip : " << _ip << " port : " << _port << std::endl;
}
void UdpServerInit()
{
// 1. 創(chuàng)建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
std::cerr << "create socket fail : " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success! " << "sockfd : " << _sockfd << std::endl;
// 2. 填充sockaddr_in結(jié)構(gòu)體
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
// 將主機(jī)序列轉(zhuǎn)換為網(wǎng)絡(luò)序列
local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_port = htons(_port);
// 3. 綁定IP,端口號(hào)
if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) != 0)
{
std::cerr << "bind fail :" << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind success :" << std::endl;
}
~UdpServer()
{
if (_sockfd > 0)
{
close(_sockfd);
}
}
private:
int _sockfd; // 套接字的文件描述符
std::string _ip; // ip地址
uint16_t _port; // 端口號(hào)
};
// udp_server.cpp
#include "udp_server.hpp"
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<UdpServer> up(new UdpServer("1.1.1.1", 8080));
up->UdpServerInit();
return 0;
}
運(yùn)行結(jié)果,可以看出bind
失敗了,這與云服務(wù)器有關(guān),云服務(wù)器不允許我們隨意綁定ip
,需要讓服務(wù)器自己指定IP地址。
當(dāng)然,云服務(wù)器不允許我們隨意綁定ip
,也有一定的道理,因?yàn)閷?duì)于一款服務(wù)器來(lái)說(shuō),這臺(tái)設(shè)備可能有多個(gè)網(wǎng)卡,這臺(tái)設(shè)備可能有多個(gè)IP,如果我們只綁定某個(gè)特定的IP就會(huì)導(dǎo)致只有某個(gè)IP能夠收到數(shù)據(jù),當(dāng)數(shù)據(jù)量很大的時(shí)候,傳輸?shù)男什⒉皇呛芨?,所以我們可以設(shè)置IP為INADDR_ANY
,設(shè)置這個(gè)IP表示:綁定本主機(jī)上面的所有IP。
INADDR_ANY
的值本質(zhì)就是0,不存在大小端的問(wèn)題,因此在設(shè)置時(shí)可以不進(jìn)行網(wǎng)絡(luò)字節(jié)序的轉(zhuǎn)換。
Ⅱ、運(yùn)行服務(wù)器
當(dāng)服務(wù)器初始化完畢后我們就可以啟動(dòng)服務(wù)器了,由于服務(wù)器是一個(gè)永不退出的進(jìn)程,所以服務(wù)器運(yùn)行以后一定是一個(gè)死循環(huán)!
讀取數(shù)據(jù)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:
- 從網(wǎng)絡(luò)中讀取數(shù)據(jù)。
參數(shù)說(shuō)明:
-
sockfd
:創(chuàng)建的套接字對(duì)應(yīng)的文件描述符,表示從該文件描述符索引的文件當(dāng)中讀取數(shù)據(jù)。 -
buf
:讀取到的數(shù)據(jù)的存放位置。 -
len
:期望讀取數(shù)據(jù)的字節(jié)數(shù)。 -
flags
:讀取的方式。一般設(shè)置為0,表示阻塞讀取。 -
src_addr
:對(duì)端網(wǎng)絡(luò)相關(guān)的屬性信息,包括協(xié)議家族、IP地址、端口號(hào)等。 -
addrlen
:src_addr
結(jié)構(gòu)體的長(zhǎng)度,返回時(shí)此值會(huì)被修改為實(shí)際讀取到的src_addr
結(jié)構(gòu)體的長(zhǎng)度,這是一個(gè)輸入輸出型參數(shù)。
返回值說(shuō)明:
- 讀取成功返回實(shí)際讀取到的字節(jié)數(shù),讀取失敗返回
-1
,同時(shí)錯(cuò)誤碼會(huì)被設(shè)置。
發(fā)送數(shù)據(jù)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
功能:
- 將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)中。
參數(shù)說(shuō)明:
-
sockfd
:創(chuàng)建的套接字對(duì)應(yīng)的文件描述符,表示將數(shù)據(jù)寫(xiě)入該文件描述符索引的文件當(dāng)中。 -
buf
:待寫(xiě)入數(shù)據(jù)的起始地址。 -
len
:期望寫(xiě)入數(shù)據(jù)的字節(jié)數(shù)。 -
flags
:寫(xiě)入的方式,一般設(shè)置為0,表示阻塞寫(xiě)入。 -
dest_addr
:對(duì)端網(wǎng)絡(luò)相關(guān)的屬性信息,包括協(xié)議家族、IP地址、端口號(hào)等。 -
addrlen
:傳入dest_addr
結(jié)構(gòu)體的長(zhǎng)度。
返回值說(shuō)明:
- 寫(xiě)入成功返回實(shí)際寫(xiě)入的字節(jié)數(shù),寫(xiě)入失敗返回-1,同時(shí)錯(cuò)誤碼會(huì)被設(shè)置。
現(xiàn)在服務(wù)端通過(guò)recvfrom
函數(shù)讀取客戶端數(shù)據(jù),我們可以先將讀取到的數(shù)據(jù)當(dāng)作字符串看待,將讀取到的數(shù)據(jù)的最后一個(gè)位置設(shè)置為’\0’,此時(shí)我們就可以將讀取到的數(shù)據(jù)進(jìn)行輸出,同時(shí)我們也可以將獲取到的客戶端的IP地址和端口號(hào)也一并進(jìn)行輸出。
需要注意的是,我們獲取到的客戶端的端口號(hào)此時(shí)是網(wǎng)絡(luò)序列,我們需要調(diào)用ntohs
函數(shù)將其轉(zhuǎn)為主機(jī)序列再進(jìn)行打印輸出。同時(shí),我們獲取到的客戶端的IP地址是整數(shù)IP,我們需要通過(guò)調(diào)用inet_ntoa
函數(shù)將其轉(zhuǎn)為字符串IP再進(jìn)行打印輸出。
// udp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
enum
{
SOCKET_ERR = 1,
BIND_ERR,
USAGE_ERR
};
class UdpServer
{
public:
UdpServer(uint16_t port)
:_port(port)
{
std::cout << "port : " << _port << std::endl;
}
void UdpServerInit()
{
// 1. 創(chuàng)建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
std::cerr << "create socket fail : " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success! " << "sockfd : " << _sockfd << std::endl;
// 2. 填充sockaddr_in結(jié)構(gòu)體
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
// 將主機(jī)序列轉(zhuǎn)換為網(wǎng)絡(luò)序列
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(_port);
// 3. 綁定IP,端口號(hào)
if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) != 0)
{
std::cerr << "bind fail :" << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind success !" << std::endl;
}
void UdpServerStart()
{
// 緩沖區(qū)
char buf[2048];
// 網(wǎng)絡(luò)信息結(jié)構(gòu)體
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 死循環(huán)不能讓服務(wù)器退出
while (true)
{
memset(&peer, 0, len);
// 收取消息
ssize_t num = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer, &len);
if (num < 0)
{
std::cerr << "recvfrom fail !" << std::endl;
continue;
}
else
{
// 結(jié)尾補(bǔ)上\0,形成C風(fēng)格字符串
buf[num] = '\0';
}
// 提取客戶端的ip和端口號(hào)
std::string peer_ip = inet_ntoa(peer.sin_addr);
uint16_t peer_port = ntohs(peer.sin_port);
std::cout << peer_ip << " | " << peer_port << " |# " << buf << std::endl;
// 發(fā)消息
sendto(_sockfd, buf, strlen(buf), 0, (struct sockaddr*)&peer, len);
}
}
~UdpServer()
{
if (_sockfd > 0)
{
close(_sockfd);
}
}
private:
int _sockfd; // 套接字的文件描述符
uint16_t _port; // 端口號(hào)
};
我們服務(wù)器啟動(dòng)的時(shí)候需要指定端口號(hào),所以這里使用了命令行參數(shù)。
// udp_server.cpp
#include "udp_server.hpp"
#include <iostream>
#include <memory>
// 使用手冊(cè)
static void usage(std::string proc)
{
std::cout << "usage\n\t" << proc << " 端口號(hào)" << std::endl;
}
// 命令行參數(shù),必須輸入兩個(gè)參數(shù),一個(gè)是程序名,一個(gè)是端口號(hào)
int main(int argc, char* argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
// 提取本地端口號(hào)
uint16_t port = atoi(argv[1]);
std::unique_ptr<UdpServer> up(new UdpServer(port));
up->UdpServerInit();
up->UdpServerStart();
return 0;
}
程序啟動(dòng)以后我們可以使用netstat -naup
顯示進(jìn)程的網(wǎng)絡(luò)信息。
netstat
常用選項(xiàng)說(shuō)明:
- -n:直接使用IP地址,而不通過(guò)域名服務(wù)器。
- -a: 顯示所有連接中的接口信息。
- -t:顯示TCP傳輸協(xié)議的連線狀況。
- -u:顯示UDP傳輸協(xié)議的連線狀況。
- -p:顯示正在使用Socket的程序識(shí)別碼和程序名稱。
運(yùn)行結(jié)果
查看網(wǎng)絡(luò)信息
netstat
命令顯示的信息中:
- Proto表示協(xié)議的類型
- Recv-Q表示網(wǎng)絡(luò)接收隊(duì)列
- Send-Q表示網(wǎng)絡(luò)發(fā)送隊(duì)列
- Local Address表示本地地址,
- Foreign Address表示外部地址
- State表示當(dāng)前的狀態(tài)
- PID表示該進(jìn)程的進(jìn)程ID
- Program name表示該進(jìn)程的程序名稱。
其中Foreign Address寫(xiě)成0.0.0.0:*表示任意IP地址、任意的端口號(hào)的程序都可以訪問(wèn)當(dāng)前進(jìn)程。
Ⅲ、關(guān)于客戶端的綁定問(wèn)題
首先,由于是網(wǎng)絡(luò)通信,通信雙方都需要找到對(duì)方,因此服務(wù)端和客戶端都需要有各自的IP地址和端口號(hào),只不過(guò)服務(wù)端需要顯示的進(jìn)行IP和端口號(hào)的綁定,而客戶端不需要顯示的進(jìn)行綁定的,這個(gè)綁定的工作由操作系統(tǒng)來(lái)進(jìn)行綁定,當(dāng)我們調(diào)用類似于sendto
這樣的接口時(shí),操作系統(tǒng)會(huì)自動(dòng)給當(dāng)前客戶端獲取一個(gè)唯一的端口號(hào)。
服務(wù)器是為了給客戶提供服務(wù)的,因此服務(wù)器必須要讓客戶知道自己的IP地址和端口號(hào),否則客戶端是無(wú)法向服務(wù)端發(fā)起請(qǐng)求的,這就是服務(wù)端要進(jìn)行顯示綁定的原因,只有一個(gè)進(jìn)程綁定了端口號(hào)之后這個(gè)端口號(hào)才真正屬于自己,因?yàn)橐粋€(gè)端口只能被一個(gè)進(jìn)程所綁定,服務(wù)器綁定一個(gè)端口就是為了獨(dú)占這個(gè)端口。
而客戶端在通信時(shí)雖然也需要端口號(hào),但客戶端一般是不進(jìn)行綁定的,客戶端訪問(wèn)服務(wù)端的時(shí)候,端口號(hào)只要是唯一的就行了,不需要明確是那個(gè)特定的端口號(hào)。
一臺(tái)設(shè)備上可以運(yùn)行很多客戶端,例如:B站客戶端綁定了8080端口號(hào),那么以后8080端口號(hào)就只能給B站客戶端使用,如果8080端口號(hào)又被淘寶客戶端綁定了并且淘寶先啟動(dòng)了,那么B站客戶端就無(wú)法啟動(dòng)了,因此客戶端端口通常是不綁定,由OS動(dòng)態(tài)分配,也就是說(shuō),客戶端每次啟動(dòng)時(shí)使用的端口號(hào)可能是變化的,此時(shí)只要我們的端口號(hào)沒(méi)有被耗盡,客戶端就永遠(yuǎn)可以啟動(dòng)。
Ⅳ、啟動(dòng)客戶端
客戶端的編寫(xiě)與服務(wù)端類似,只不過(guò)客戶端不需要我們進(jìn)行綁定工作的,此外作為一個(gè)客戶端,它必須知道它要訪問(wèn)的服務(wù)端的IP地址和端口號(hào),因此在我們啟動(dòng)客戶端時(shí)中需要引入服務(wù)端的IP地址和端口號(hào)。
客戶端和服務(wù)端在功能上是相互補(bǔ)充的,我們上面的服務(wù)器是在讀取客戶端發(fā)來(lái)的數(shù)據(jù)然后回發(fā)回去,那么這里我們的客戶端就應(yīng)該向服務(wù)端發(fā)送數(shù)據(jù),然后接收服務(wù)器回發(fā)的數(shù)據(jù)。
// client.cpp
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
enum
{
SOCKET_ERR = 1,
BIND_ERR,
USAGE_ERR
};
// 使用手冊(cè)
static void usage(std::string proc)
{
std::cout << "usage\n\t" << proc << " IP 端口" << std::endl;
}
// 命令行參數(shù),必須輸入三個(gè)參數(shù),一個(gè)是程序名,一個(gè)是IP,一個(gè)是端口號(hào)
int main(int argc, char* argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
// 1. 得到服務(wù)器的IP和端口
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
// 2. 創(chuàng)建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "create socket fail : " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// 3. 填充server結(jié)構(gòu)體
struct sockaddr_in server;
socklen_t len = sizeof(server);
memset(&server, 0, len);
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
server.sin_port = htons(server_port);
// 4. 業(yè)務(wù)處理
std::string message;
char buf[2048];
while (true)
{
std::cout << "[pan的服務(wù)器] :> ";
getline(std::cin, message);
// 發(fā)送消息
// 在我們首次調(diào)用系統(tǒng)調(diào)用發(fā)送數(shù)據(jù)時(shí),OS會(huì)隨機(jī)選擇一個(gè)端口號(hào) + 自己的IP進(jìn)行bind
ssize_t num = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);
if (num < 0)
{
std::cerr << "sendto fail !" << std::endl;
continue;
}
struct sockaddr_in temp;
socklen_t temp_len = sizeof(temp);
memset(&temp, 0, temp_len);
// 收消息
num = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&temp, &temp_len);
if (num < 0)
{
std::cerr << "recvfrom fail !" << std::endl;
continue;
}
else
{
buf[num] = '\0';
}
std::cout << "server's message | " << buf << std::endl;
}
return 0;
}
Ⅴ、本地測(cè)試
現(xiàn)在服務(wù)端和客戶端的代碼都已經(jīng)編寫(xiě)完畢,我們可以先進(jìn)行本地測(cè)試,現(xiàn)在我們運(yùn)行服務(wù)器時(shí)指明端口號(hào)為8080,再運(yùn)行客戶端,此時(shí)客戶端要訪問(wèn)的服務(wù)器的IP地址就是本地環(huán)回127.0.0.1地址,服務(wù)端的端口號(hào)就是8080。
-
127.0.0.1
:本地環(huán)回,表示當(dāng)前主機(jī)的地址,通常用來(lái)進(jìn)行本地通信或測(cè)試。
我們要讓服務(wù)端先運(yùn)行,然后再讓客戶端運(yùn)行,之后提示我們進(jìn)行輸入,當(dāng)我們?cè)诳蛻舳溯斎霐?shù)據(jù)后,客戶端將數(shù)據(jù)發(fā)送給服務(wù)端,此時(shí)服務(wù)端再將收到的數(shù)據(jù)打印輸出后回發(fā),這時(shí)我們?cè)诜?wù)端和客戶端的窗口都能看到我們輸入的內(nèi)容。
此時(shí)我們?cè)儆?code>netstat命令查看網(wǎng)絡(luò)信息,可以看到服務(wù)端的端口是8080,客戶端的端口是44777。這里客戶端能被netstat命令查看到,說(shuō)明客戶端也已經(jīng)動(dòng)態(tài)綁定成功了,這就是我們所謂的網(wǎng)絡(luò)通信。
Ⅵ、網(wǎng)絡(luò)測(cè)試
如果你是云服務(wù)器,請(qǐng)確保你想使用的端口已經(jīng)開(kāi)放,下面是騰訊云的云服務(wù)器開(kāi)放端口的方法:
好了,我們開(kāi)始進(jìn)行網(wǎng)絡(luò)測(cè)試:
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-735045.html
你可以將此客戶端軟件給更多的人,讓它們都能夠連接你的服務(wù)器,進(jìn)行網(wǎng)絡(luò)通信。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-735045.html
到了這里,關(guān)于【網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(一)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!