一、網絡基礎1
網絡的劃分
- 區域網路(覆寫範圍在1000m内)
- 城域網(覆寫範圍在20㎞内)
- 廣域網(覆寫範圍大于20km),(網際網路 / 網際網路是更大的國際性的廣域網- - - 容災性更強,以太網 / 令牌環網 是組網方式)
IP位址
uint32_t - - - 無符号4個位元組的整數
1、在網絡中作為主機的唯一辨別,網絡中主機之間的定位(哪個主機與哪個主機之間進行通信),通過IP位址進行辨別。
2、網絡中每條資料中都會包含:源端的IP位址 / 對端的IP位址;
3、ipv4 : uint32_t - - -DHCP/NAT
ipv6 : uint8_t addr[16] - - - 推廣度還很低
端口号
uint16_t - - -無符号2個位元組的整數
在一台主機上唯一辨別一個程序 - - - - 編寫通信程式的時候,必須告訴計算機,發往哪個端口的資料應該交給我處理;
一個端口隻能被一個程序占用,然而一個程序可以使用多個端口
在網絡通信的每條資料中,都會包含有 源端端口 / 對端端口 - - - 辨別了這個資料從哪個程序發送出來,要交給哪個程序處理。
網絡通信協定
網絡通信證的資料格式約定,遵循統一通信協定标準,才能實作實質通信,實作網絡互聯
協定分層
根據通信場景的不同,提供的服務不同,使用的協定不同進行的層次劃分
典型協定分層
OSI七層參考模型:應用層、表示層、會話層、傳輸層、網絡層、鍊路層、實體層。
TCP/IP五層模型:應用層、傳輸層、網絡層、鍊路層、實體層。
- 應用層:負責應用程式之間如何溝通;HTTP / FTP / DNS / DHCP…
- 傳輸層:負責程序之間的資料傳輸; TCP / UDP
- 網絡層:負責位址管理與路由選擇; IP / 路由器
- 鍊路層:負責相鄰裝置之間的資料傳輸; 以太網協定 / 交換機
- 實體層:負責實體光電信号的傳輸; 以太網協定 / 集線器
網絡通信資料的封裝與分用流程
主機位元組序:一個主機位元組序的大小端取決于cpu架構 - - - X86 / MIPS
int a =0x 01 02 03 04 -> 高位 000000001 00000010 00000011 00000100 低位
uchar *b = (uchar *)&a 記憶體低位址 b[0] b[1] b[2] b[3] 記憶體高位址
大端位元組序:低位址存高位 b[0]=01、 b[1]=02、 b[2]=03、 b[3]=04
小端位元組序:低位址存低位 b[0] =04、 b[1]=03、 b[2]=02、 b[3]=01
網絡位元組序:網絡通信中的位元組序标準(将自己的資料位元組序轉換成标準位元組序再進行傳輸) - - - 避免因為主機位元組序不同造成是資料二義。
- 位元組序:cpu對記憶體中資料存儲是順序;
- 主機位元組序的分類:大端位元組序、小端位元組序;
主機位元組序跟網絡通信的關系:不同主機位元組序的主機進行通信容易造成資料二義性。
網絡通信中,存儲單元大于一個位元組的資料類型需要進行網絡位元組序的轉換。
判斷一個主機的位元組序:
union{int a; char b;} tmp_t --- 聯合體成員共用同一份空間;
tmp_t tmp; tmp.a=1 if(tmp.b==1){小端}
網絡通信程式編寫的時候,到底在傳輸層用 tcp 協定好還是 udp 協定好?
tcp:傳輸控制協定,面向連接配接,可靠傳輸,面向位元組流。(tcp 保證可靠傳輸,但是傳輸速度沒有 udp 快)。
udp:使用者資料協定,無連接配接,不可靠,面向資料報。(tcp應用于安全性要求高的場景/udp應用于實時性要求高的場景)。
二、udp套接字程式設計
網絡通信中的資料,必須包含:源端IP、源端端口、對端IP、對端端口、協定。
udp通信程式設計
用戶端不主動綁定位址端口,是為了降低端口沖突的機率。
udp套接字(socket)接口介紹
1、建立套接字
domain:位址域,确定本次socket通信使用哪種協定版本的位址結果,不同協定版本有不同的位址結構。AF_INT IPV4網絡協定。
type:套接字類型(流式套接字- - -SOCK_STREAM / 資料報套接字- - -SOCK_DGRAM)
protocol:協定類型(通常就是傳輸層協定的選擇IPPROTO_TCP / IPPROTO_UDP),預設為0,流式預設tcp / 資料報預設udp。
傳回值:檔案描述符 - - -非負整數- - -套接字所有其它接口的操作句柄;失敗傳回 -1;
2、為套接字綁定位址資訊
sockfd:建立套接字傳回的操作句柄;
addr:要綁定的位址資訊;
len:要綁定的位址資訊長度;
bind 可以綁定不同的位址結構,為了實作接口統一,是以使用者定義位址結構的時候,定義自己需要的位址結構(例如:ipv4就使用struct sockaddr_in),但是進行綁定的時候,統一類型強轉成為sockaddr* 類型。
bind(fd, struct sockaddr *addr, len);
{
if(addr->sa_family == AF_inet)
{
//綁定IPV4位址資訊,這個結構體按照sockaddr_in進行解析
}
else if(addr->sa_family == AF_INET6)
{//IPV6位址綁定,按照IPV6位址結構解析}
else if(addr->sa_family == AF LOCAL)
{}
}
3、接收資料
不僅僅接收資料,還要通過接收得知這個數是誰發的,以便于進行回複。
sockfd::socket操作句柄;
buf:一塊緩沖區,用于接收從接收緩沖區中取出的資料;
len:想要接收的資料長度;
flag:操作選項标志,預設為0,表示阻塞操作;
peer_addr:發送方的位址資訊;
addrlen:想要擷取的位址資訊長度以及傳回實際長度;
傳回值:成功傳回實際接收到的資料位元組長度;失敗傳回-1;
4、發送資料
sockfd:socket操作句柄;
data:要發送的資料首位址;
len:要發送資料長度;
flag:預設為0,表示阻塞操作;
peer_addr:接收方的位址資訊;
addrlen:位址資訊長度;
傳回值:成功傳回實際發送的資料的位元組長度,失敗傳回-1;
5、關閉套接字
使用c++封裝一個 UdpSocket 類,執行個體化的每一個對象都是udp 套接字,并且能夠通過成員接口實作Udp通信流程。
class UspSocket
{
public:
bool Socket(); //建立套接字
bool Bind(const std::string &ip, uint16_t prot); //為套接字綁定位址資訊
bool Recv(std::string *buf, std::string *ip, uint16_t *port); //接收資料,擷取發送端位址資訊
bool Send(const std::string &data, onst std::string &ip, const uint16_t port); //發送資料
bool Close(); //關閉套接字
private:
int _sockfd;
}
網絡位元組序的轉換接口
uint32_t htonl(uint32_t hostlong); //hton ---主機位元組序到網絡位元組序的轉換
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong); //ntoh --- 網絡位元組序到主機位元組序的轉換
uint16_t ntohs(uint16_t netshort);
in_addr_t inet_addr(const char *cp); //将字元串的點分十進制IP位址轉換成網絡位元組序的整數IP位址
char *inet_ntoa(struct in_addr in); //将網絡位元組序的整數IP位址轉換成字元串點分十進制IP位址
const char *inet_ntop(int af,const void *src, char *dst, socklen_t size); //将網絡位元組序的整數IP位址,轉換成字元串的IP位址(相容IPV6)
int inet_pton(int af, const char *src, void *dst); //将字元串的IP位址轉換成網絡位元組序的整數IP位址
udpsocket服務端(C語言)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>//位址結構體定義的頭檔案
#include <arpa/inet.h>//位元組序轉換接口的頭檔案
#include <sys/socket.h>//套接字接口的頭檔案
int main()
{
uint16_t port = 9000;
char *ip = "172.31.43.144";
//建立套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0) {
perror("socket error");
return -1;
}
//綁定位址資訊: 1.定義位址結構 / 2. 位址資訊指派 / 3. 進行綁定
struct sockaddr_in addr;
addr.sin_family = AF_INET;//指派位址域類型
addr.sin_port = htons(port);//指派端口
addr.sin_addr.s_addr = inet_addr(ip);//指派IP位址
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(sockfd, (struct sockaddr*)&addr, len);
if (ret < 0) {
perror("bind error");
return -1;
}
//接收資料: 不但要接收資料,還要接收發送方位址資訊
char tmp[4096] = {0};
struct sockaddr_in cli_addr;
char cli_ip[24] = {0};
uint16_t cli_port = 0;
ret = recvfrom(sockfd, tmp, 4096, 0, (struct sockaddr*)&cli_addr, &len);
if (ret < 0) {
perror("recvfrom error");
return -1;
}
strcpy(cli_ip, inet_ntoa(cli_addr.sin_addr));
cli_port = ntohs(cli_addr.sin_port);
//發送資料: 将接收到的資料在回送給用戶端
ret = sendto(sockfd, tmp, ret, 0, (struct sockaddr*)&cli_addr, len);
if (ret < 0) {
perror("sendto error");
return -1;
}
//關閉套接字
close(sockfd);
return 0;
}
udpsocket服務端(C++語言)
#include <cstdio>
#include <string>
#include <netinet/in.h>//包含位址結構資訊
#include <arpa/inet.h>//位元組序轉換接口
#include <sys/socket.h>//套接字接口資訊
class UdpSocket {
public:
UdpSocket():_sockfd(-1){}
bool Socket() {//建立套接字
//socket(位址域, 套接字類型, 協定類型)
_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (_sockfd < 0) {
perror("socket error");
return false;
}
return true;
}
// 為套接字綁定位址資訊
bool Bind(const std::string &ip, uint16_t port) {
//定義IPV4位址結構 struct sockaddr_in
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);//htons将主機位元組序短整型資料轉換為網絡位元組序資料
addr.sin_addr.s_addr = inet_addr(ip.c_str());//将字元串IP位址轉換為網絡位元組序
//bind(描述符, 位址資訊, 位址資訊長度)
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0) {
perror("bind error");
return false;
}
return true;
}
//接收資料,擷取發送端位址資訊
bool Recv(std::string *buf, std::string *ip=NULL, uint16_t *port=NULL) {
//recvfrom(套接字句柄,接收緩沖區,資料長度,标志, 源端位址,位址長度)
struct sockaddr_in peer_addr;
socklen_t len = sizeof(struct sockaddr_in);
char tmp[4096] = {0};
int ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&peer_addr, &len);
if (ret < 0) {
perror("recvfrom error");
return false;
}
buf->assign(tmp, ret); // assign從指定字元串中截取指定長度的資料到buf中
if (port != NULL) {
*port = ntohs(peer_addr.sin_port);//網絡位元組序到主機位元組序的轉換
}
if (ip != NULL) {
*ip = inet_ntoa(peer_addr.sin_addr);//網絡位元組序到字元串IP位址的轉換
}
return true;
}
// 發送資料
bool Send(const std::string &data, const std::string &ip, const uint16_t port) {
//sendto(套接字句柄,資料首位址,資料長度,标志,對端位址資訊,位址資訊長度)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = sendto(_sockfd, data.c_str(), data.size(), 0,
(struct sockaddr*)&addr, len);
if (ret < 0) {
perror("sendto error");
return false;
}
return true;
}
bool Close(){
if (_sockfd > 0) {
close(_sockfd);
_sockfd = -1;
}
return true;
}// 關閉套接字
private:
int _sockfd;
};
udpsocket用戶端(C語言)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main()
{
//1.建立套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0) {
perror("socket error");
return -1;
}
//2.綁定位址資訊(不推薦)
//3.發送資料: 要給誰發送什麼資料--發送的對端位址一定是服務端綁定的位址
struct sockaddr_in srv_addr;
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(9000);
srv_addr.sin_addr.s_addr = inet_addr("172.31.43.144");
socklen_t len = sizeof(struct sockaddr_in);
char tmp[1024] = {0};
fgets(tmp, 1024, stdin);
sendto(sockfd, tmp, strlen(tmp), 0, (struct sockaddr*)&srv_addr, len);
//4.接收資料
char buf[1024] = {0};
//對于用戶端來說,本身久知道服務端位址,是以其實根本不用接收服務端位址資訊
recvfrom(sockfd, buf, 1023, 0, (struct sockaddr*)&srv_addr, &len);
printf("server say:%s\n", buf);
//5.關閉套接字
close(sockfd);
return 0;
}
udpsocket用戶端(C++語言)
#include <iostream>
#include <string>
#include "udpsocket.hpp"
#define CHECK_RET(q) if((q)==false){return -1;}
int main (int argc, char *argv[])
{
//用戶端參數擷取的IP位址是服務端綁定的位址,也就是用戶端發送資料的目标位址
//不是為了自己綁定的
if (argc != 3) {
std::cout << "Usage: ./udp_cli ip port\n";
return -1;
}
std::string srv_ip = argv[1];
uint16_t srv_port = std::stoi(argv[2]);
UdpSocket cli_sock;
//建立套接字
CHECK_RET(cli_sock.Socket());
//綁定位址(不推薦)
while(1) {
//發送資料
std::cout << "client say:";
std::string buf;
std::cin >> buf;
if (buf == "quit") {
break;
}
CHECK_RET(cli_sock.Send(buf, srv_ip, srv_port));
//接收資料
buf.clear();
CHECK_RET(cli_sock.Recv(&buf));//預設參數可以不用賦予
std::cout << "server say: " << buf << std::endl;
}
//關閉套接字
cli_sock.Close();
//...
return 0;
}
三、tcp套接字程式設計
面向連接配接,可靠傳輸,面向位元組流。
面向連接配接:必須建立了連接配接保證雙方都具有資料收發的能力,才能開始通信;(udp是隻需要知道對端位址就可以直接發送資料)。
tcp套接字(socket)接口介紹
1、建立套接字
type:SOCK_DGRAM - - - 資料報套接字 / SOCK_STREAM - - -流式套接字
protocol:IPPROTO_TCP
2、綁定位址資訊
3、開始監聽
sockfd:将哪個套接字設定為監聽狀态,并且監聽狀态後可以開始接收用戶端連接配接請求。
backlog:同一時間的并發連接配接數,決定同一時間最多接收多少個用戶端的連接配接請求。
4、擷取建立連接配接
從已完成連接配接隊列中取出一個 socket,并且傳回這個socket的描述符操作句柄。
sockfd:監聽套接字,表示要擷取哪個 tcp 服務端套接字的建立連接配接;
cli——addr:這個新的套接字對應的用戶端位址資訊
len:位址資訊長度;
傳回值:建立socket 套接字的描述符 - - - 外部程式中的操作句柄;
5、接受資料 / 發送資料
因為 tcp 通信套接字中已經辨別了五元組,是以不需要接收資料的時候擷取對方位址資訊,發送資料的時候也不需要指定對方的位址資訊。
預設阻塞,沒有資料則等待,連接配接斷開傳回0,不再阻塞
預設阻塞,緩沖區資料滿了則等待,連接配接斷開則觸發SIGPIPE異常
6、關閉套接字
7、向服務端發送連接配接請求
srv_addr:服務端位址資訊 - - - 給誰發送連接配接請求
connect這個接口也會在sockfd 的套接字socket 中描述對端位址資訊
封裝一個TcpSocket類
每一個執行個體化的對象都是一個socket 通信連接配接,通過這個對象的成員完成通信流程
class TcpSocket{
public:
TcpSocket();
bool Socket();
bool Bind(const std::string &ip, uint16_t port);
bool Listen(int backlog = MAX_LISTEN);
bool Accept(TcpSocket *new_sock, std::string *ip = NULL, uint16_t *port = NULL);
bool Recv(std::string *buf);
bool Send(const std::string &data);
bool Close();
bool Connect(const std::string &ip, uint16_t port);
private:
int _sockfd;
}
while(1)
{
-
1、擷取新連接配接;
一旦擷取到一個新連接配接,就啟動一個新的執行流(多線程/多程序),讓這個新的執行流去與用戶端進行通信
a、因為沒有新連接配接到來的阻塞,不會影響與用戶端的通信
b、與用戶端通信的阻塞不會影響擷取新連接配接
- 2、通過新連接配接接收指定用戶端資料;
- 3、通過新連接配接向指定用戶端發送資料;
}
目前,在一個執行流中,完成好多個操作,擷取新連接配接、接收資料局、發送資料,然而這幾個操作都有可能導緻流程阻塞,因為我們在固定流程下,有可能會對沒有資料到來的socket進行操作,是以導緻阻塞。
tcppsocket(C++頭檔案hpp)
#include <cstdio>
#include <string>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
//listen的第二個參數決定同一時間能夠接收多少用戶端連接配接
//并不決定整體通信能夠接收多少用戶端連接配接
#define MAX_LISTEN 5
#define CHECK_RET(q) if((q)==false){return -1;}
class TcpSocket {
public:
TcpSocket ():_sockfd(-1){}
bool Socket() {
//socket(位址域, 套接字類型, 協定類型)
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0) {
perror("socket error");
return false;
}
return true;
}
bool Bind(const std::string &ip, uint16_t port) {
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0) {
perror("bind error");
return false;
}
return true;
}
bool Listen(int backlog = MAX_LISTEN) {
//listen(套接字描述符, 最大并發連接配接數)
int ret = listen(_sockfd, backlog);
if (ret < 0) {
perror("listen error");
return false;
}
return true;
}
bool Accept(TcpSocket *new_sock, std::string *ip=NULL, uint16_t *port=NULL) {
//建立套接字描述符 = accept(監聽套接字描述符, 用戶端位址資訊,位址長度);
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int new_fd = accept(_sockfd, (struct sockaddr*)&addr, &len);
if (new_fd < 0) {
perror("accept error");
return false;
}
new_sock->_sockfd = new_fd;
if (ip != NULL) {
(*ip) = inet_ntoa(addr.sin_addr);
}
if (port != NULL) {
*port = ntohs(addr.sin_port);
}
return true;
}
bool Recv(std::string *buf) {
//recv(通信套接字描述符,緩沖區首位址,接收資料長度, 标志位-0阻塞)
char tmp[4096] = {0};
int ret = recv(_sockfd, tmp, 4096, 0);
if (ret < 0) {
perror("recv error");
return false;
}else if (ret == 0) {//recv預設阻塞,沒有資料就會等待,傳回0,表示連接配接斷開
printf("connection broken\n");
return false;
}
buf->assign(tmp, ret);
return true;
}
bool Send(const std::string &data) {
//send(描述符,要發送資料首位址,要發送的資料長度,标志位-0阻塞)
int ret = send(_sockfd, data.c_str(), data.size(), 0);
if (ret < 0) {
perror("send error");
return false;
}
return true;
}
bool Close() {
if (_sockfd > 0) {
close(_sockfd);
_sockfd = -1;
}
return true;
}
bool Connect(const std::string &ip, uint16_t port) {
//向服務端發起連接配接
//connect(描述符, 服務端位址資訊, 位址資訊長度)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = connect(_sockfd, (struct sockaddr *)&addr, len);
if (ret < 0) {
perror("connect error");
return false;
}
return true;
}
private:
int _sockfd;
};
tcppsocket服務端(C++語言)
/*1. 建立套接字
2. 綁定位址資訊
3. 開始監聽
4. 擷取新連接配接
5. 收發資料
6. 關閉套接字
*/
#include <iostream>
#include "tcpsocket.hpp"
int main (int argc, char *argv[])
{
if (argc != 3) {
std::cout << "Usage: ./tcp_srv ip port\n";
return -1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());//建立監聽套接字
CHECK_RET(lst_sock.Bind(ip, port));//為監聽套接字綁定位址
CHECK_RET(lst_sock.Listen());//開始監聽
while(1) {
TcpSocket new_sock;
bool ret = lst_sock.Accept(&new_sock);//通過監聽套接字擷取建立連接配接
if (ret == false) {
continue;//服務端不能因為或一個建立套接字失敗就退出
}
std::string buf;
new_sock.Recv(&buf);//通過建立連接配接與指定用戶端進行通信
std::cout << "client say: " << buf << std::endl;
buf.clear();
std::cout << "server say: ";
std::cin >> buf;
new_sock.Send(buf);
}
lst_sock.Close();
return 0;
}
tcppsocket用戶端(C++語言)
/*
實作tcp用戶端流程
1. 建立套接字
2. 綁定位址資訊(不推薦)
3. 向服務端發起連接配接請求
4. 收發資料
5. 關閉套接字
*/
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include "tcpsocket.hpp"
void sigcb(int no) {
//SIGCHLD信号是一個非可靠信号,有可能丢失
//是以在一次信号進行中,就需要處理到沒有子程序退出為止
while(waitpid(-1, NULL, WNOHANG) > 0);//傳回值大于0,表示有子程序退出
}
int main (int argc, char *argv[])
{
if (argc != 3) {
std::cout << "Usage: ./tcp_srv ip port\n";
return -1;
}
signal(SIGCHLD, sigcb);
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());//建立監聽套接字
CHECK_RET(lst_sock.Bind(ip, port));//為監聽套接字綁定位址
CHECK_RET(lst_sock.Listen());//開始監聽
while(1) {
TcpSocket new_sock;
bool ret = lst_sock.Accept(&new_sock);//通過監聽套接字擷取建立連接配接
if (ret == false) {
continue;//服務端不能因為或一個建立套接字失敗就退出
}
int pid = fork();//子程序複制父程序,父程序有的子程序都有
if (pid == 0) {
while(1) {
std::string buf;
new_sock.Recv(&buf);//通過建立連接配接與指定用戶端進行通信
std::cout << "client say: " << buf << std::endl;
buf.clear();
std::cout << "server say: ";
std::cin >> buf;
new_sock.Send(buf);
}
new_sock.Close();//new_sock父子程序各有各的 ,父子程序資料獨有
exit(0);
}
new_sock.Close();//父程序關閉自己的不使用的socket,不影響子程序
}
lst_sock.Close();
return 0;
}