上次我們大概介紹了一些關于網絡的基礎知識,這次我們利用編程來深入學習一下
一:套接字Socket
1.1什么是Socket

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
socket?
我們可以用man 直接查這個socket,第一個參數是域或者協議家族,什么意思呢,就是以下這些
看它第二列目的,抵押給是本地通信,第二哥就是域間通信,第三個是IPV4,我們今天就用IPv4規定我們的IP地址。
第二個參數是代表套接字類型,也就是我們使用的TCP協議,還是UDP協議,我們今天用的UDP協議




這個是什么呢,##這個符號,叫做拼接符,什么啥意思呢,就是前面的參數拼接我后面的family,也就是現在它現在傳的這個參數是sa_ ,那么拼接起來就是sa_family。
也就是sa_family 代表 用AF_INET 等類型初始化
但是我們剛才也說了sockaddr ,是作為最后的網絡的統一的接口的,也就是說,我們在UDP通信的時候今天申請的是網絡通信,所以用的是sockaddr_in,所以我們要用sockaddr_in這個結構體把網絡信息存進去,最后強轉成sockaddr。
我們看這個結構體,第一個就是 sin_family(剛才說的拼接)
第二個 sin_port ->端口號
第三個 sin_addr -> IP地址
所以我們要把我們這個服務端的協議族,端口號,IP地址存進去。
struct sockaddr_in local;bzero(&local, sizeof(local)); // 最好是先清零再去給它賦值 用memset也可以local.sin_family = AF_INET;local.sin_port = ::htons(_port); // 因為字節序的問題 ,最好是要給轉換成網絡的大端local.sin_addr.s_addr = inet_addr(_ip.c_str());
當大家寫的時候會發現,并不能直接將端口號直接賦值給sin_port,因為什么呢?因為我們上次說了網絡中也是存在大小端的,就是有字節序的問題,所以我們要先轉換成大端,也就是用htons函數
也不能直接把IP地址直接賦值給sin_addr,為什么呢?因為sin_addr它本身是一個結構體,我們還要在結構體里面找結構體成員進行賦值。
,因為我申請的IP用的是string 存放的,所以我想把這個村給s_addr(類型是32位4字節),就要轉換
,用inet_addr()把char*轉換成uint32_t 。
把網絡信息存好,就可以進行綁定了。
//int n = bind(_sockfd, CONV(&local), sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";
1.5 接受網絡消息--recvfrom
因為我們的UDP的傳輸報文方式是數據報形式的,所以用recvfrom
第一個參數依舊是老朋友,文教描述符,
第二個是一個void* buffer,意思就是存放信息的地方,
第三個是長度,
第四個默認位0就行,
第五個依舊就是統一接口sockaddr,代表發送端的網絡信息(IP+Port)
第六個的socklen_t 代表sockaddr 的大小是多大的,用一個socklen_t的變量存起來然后取地址就可以。
ssize_t n = ::recvfrom ( _sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&len);
1.6 sendto 發送消息
依舊是因為UDP傳輸的數據報,選擇sendto
第一個參數依舊是老朋友,文教描述符,
第二個是一個void* buffer,意思就是存放信息的地方,
第三個是長度,
第四個默認位0就行,
第五個依舊就是統一接口sockaddr,代表是發送端的網絡信息
第六個的是sockaddr的大小,直接用sizeof就可以。
然后我們處理一下接受到的消息,之后再給客戶端發送過去即可。
void Start(){LOG(LogLevel::INFO) << "start";while(true){//要接受客戶端的信息//ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, *addrlen);char inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer) ;ssize_t n = ::recvfrom ( _sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&len);if(n>0){uint16_t clientport = ::ntohs(peer.sin_port);std::string clientip = ::inet_ntoa(peer.sin_addr);inbuffer[n] = 0;std::string clientinfo = clientip + ":" + std::to_string(clientport) + "#" + inbuffer;LOG(LogLevel::DEBUG) << clientinfo;//接受消息后 還有給客戶端發送數據std::string echo_string = "echo#" ; echo_string += inbuffer;::sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,CONV(&peer),len);}else{LOG(LogLevel::FATAL) << "recvfrom: " << strerror(errno);}}
我們再說一個轉換函數,這個要比inet_ntoa()函數轉換更加安全
_ip = ::inet_ntop(AF_INET, &_local.sin_addr, ipbuffer, sizeof(ipbuffer));
inet_ntoa 本質呢是在內部有一個靜態存儲區,如果是多線程調用它,很容易進行覆蓋,所以它并不安全,inet_ntop 這個函數是讓你自己申請一個靜態區,所以由程序員自己掌握,相對安全。
1.6 是否要綁定固定的 IP 和端口號
端口號有一定的定義范圍
IP地址的定義范圍
IPv4地址是一個32位的二進制數,通常被分為四段表示,每段8位,并以點分十進制(dotted-decimal notation)的形式表示,即每個字節轉換為對應的十進制數字,各字節間用點號隔開。范圍:0.0.0.0~255.255.255.255
特殊用途的IPv4地址范圍包括:
私有地址:這些地址范圍專門保留用于內部網絡,不會在全球互聯網路由中出現。10.0.0.0 ~?10.255.255.255? ? 172.16.0.0 ~?172.31.255.255? ? ?192.168.0.0 ~ 192.168.255.255
回環地址:用于本機回環測試,通常使用127.0.0.1代表本地主機。
127.0.0.0 到 127.255.255.255
自動專用IP尋址:當無法從DHCP服務器獲得配置時,系統可能會自動配置此范圍內的地址。
169.254.0.0 到 169.254.255.255。
我們要不要再服務器中,把某個進程也就是它的端口號,和IP綁定在一起呢
答案是,不要,因為一個公司里的服務器,會有很多的IP地址,我們要給同一個端口發送消息,那么經過不同的IP,同一個端口號,都能給這個進程發送信息,但是如果你綁定了特定的IP,那么只有通過這個IP,才能給這個進程發送消息,所以我們不能綁定IP。
1.7 客戶端
在我們學習完服務端之后,客戶端就很簡單了。
首先依舊是創建一個網絡通信的文件,和上面一樣。
然后區別來了:客戶端用不用綁定網絡信息呢??、
答案是:不用。為什么呢?難道客戶端發送不需要對方知道自己的網絡信息嗎?并不是這樣的,只是不用綁定,是因為系統回自動給客戶端自由隨機分配一個,為什呢?假設你的淘寶進程 你給綁定了80的端口號,而抖音也要這個端口號,那么不就起了沖突嗎?而讓系統自己分配,就可以避免這種沖突。只是不用手動綁定,而不是沒有網絡信息。
接著是一樣的發送消息,接受消息。
這個的設計思路是,直接默認手輸服務端的IP和端口,所以我們要用 argc 和 argv 兩個參數。
if (argc != 3){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}ENABLE_CONSOLE_LOG();// ip + portstd::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = socket(AF_INET,SOCK_DGRAM,0);if(sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}struct sockaddr_in tmp;memset(&tmp, 0, sizeof(tmp));tmp.sin_family = AF_INET;tmp.sin_port=::htons(serverport);tmp.sin_addr.s_addr = inet_addr(serverip.c_str());while(true){std::cout<<"輸入你要說的話"<<std::endl;std::string tstring;std::getline(std::cin,tstring);int n = ::sendto(sockfd,tstring.c_str(),tstring.size(),0,CONV(&tmp),sizeof(tmp));if(n<0){LOG(LogLevel::FATAL)<<"Client send to false";}char inbuffer[1024];struct sockaddr_in server;socklen_t len = sizeof(server);int m = ::recvfrom(sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&server),&len);if(m>0){inbuffer[m] = 0;std::cout<<inbuffer<<std::endl;}}
1.8 實現云平臺和云平臺直接的通信
那么你直接開兩個終端,一個運行客戶端,一個運行服務端,默認服務端 的端口號8080或者8888都可以。然后直接通信就可以了
1.9 云平臺和Windows 的通信
首先你要在你的云平臺的管理器那里,申請UDP通信,要保證你的UDP端口是可以用的。接下來給大家分享一個Windows端作為用戶端的代碼,云平臺依舊使用我們剛才的服務端的代碼就可以
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
std::string serverip = ""; //自己云平臺的IP
uint16_t serverport = 8080;//默認一個端口號就可以
int main()
{WSADATA wsd;WSAStartup(MAKEWORD(2, 2), &wsd);struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); //?server.sin_addr.s_addr = inet_addr(serverip.c_str());SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == SOCKET_ERROR){std::cout << "socker error" << std::endl;return 1;}std::string message;char buffer[1024];while (true){std::cout << "Please Enter@ ";std::getline(std::cin, message);if (message.empty()) continue;sendto(sockfd, message.c_str(), (int)message.size(), 0,(struct sockaddr*)&server, sizeof(server));struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}closesocket(sockfd);WSACleanup();return 0;
}
網絡的代碼一樣,只是在Windows下,某些文件需要提前配置和打開關閉。
2.0奉上全部代碼
UDPClient.cc
#include<iostream>
#include "Log.hpp"
#include "UdpServer.hpp"
#include <string>
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace LogModule;
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}ENABLE_CONSOLE_LOG();// ip + portstd::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = socket(AF_INET,SOCK_DGRAM,0);if(sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}struct sockaddr_in tmp;memset(&tmp, 0, sizeof(tmp));tmp.sin_family = AF_INET;tmp.sin_port=::htons(serverport);tmp.sin_addr.s_addr = inet_addr(serverip.c_str());while(true){std::cout<<"輸入你要說的話"<<std::endl;std::string tstring;std::getline(std::cin,tstring);int n = ::sendto(sockfd,tstring.c_str(),tstring.size(),0,CONV(&tmp),sizeof(tmp));if(n<0){LOG(LogLevel::FATAL)<<"Client send to false";}char inbuffer[1024];struct sockaddr_in server;socklen_t len = sizeof(server);int m = ::recvfrom(sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&server),&len);if(m>0){inbuffer[m] = 0;std::cout<<inbuffer<<std::endl;}}return 0;
}
UDPServer.hpp
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const static std::string fault_ip = "127.0.0.1";
const static uint16_t fault_port = 8080;
class UDPServer
{
public:UDPServer(std::string ip = fault_ip, uint16_t port = fault_port): _ip(ip), _port(port), _isrunning(false), _sockfd(-1){}void InitServer(){_sockfd = socket(AF_INET, SOCK_DGRAM, 0); //這是創建好了一個網絡上的文件if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket create false";Die(USAGE_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;struct sockaddr_in local;bzero(&local, sizeof(local)); // 最好是先清零再去給它賦值 用memset也可以local.sin_family = AF_INET;local.sin_port = ::htons(_port); // 因為字節序的問題 ,最好是要給轉換成網絡的大端local.sin_addr.s_addr = inet_addr(_ip.c_str());//要綁定網絡上的信息 你要知道是誰傳的 ip + portint n = bind(_sockfd, CONV(&local), sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void Start(){LOG(LogLevel::INFO) << "start";_isrunning = true;while(true){//要接受客戶端的信息// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,// struct sockaddr *src_addr, *addrlen);char inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer) ;ssize_t n = ::recvfrom ( _sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&len);if(n>0){uint16_t clientport = ::ntohs(peer.sin_port);std::string clientip = ::inet_ntoa(peer.sin_addr);inbuffer[n] = 0;std::string clientinfo = clientip + ":" + std::to_string(clientport) + "#" + inbuffer;LOG(LogLevel::DEBUG) << clientinfo;//接受消息后 還有給客戶端發送數據std::string echo_string = "echo#" ; echo_string += inbuffer;::sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,CONV(&peer),len);}else{LOG(LogLevel::FATAL) << "recvfrom: " << strerror(errno);}}}~UDPServer(){}private:int _sockfd; // 建立的網絡文件描述符std::string _ip;uint16_t _port;bool _isrunning;
};#endif
UDPServer.cc
#include<iostream>
#include "Log.hpp"
#include "UdpServer.hpp"
#include <string>
#include "Common.hpp"
using namespace LogModule;
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}ENABLE_CONSOLE_LOG();std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);std::unique_ptr<UDPServer> svr_uptr = std::make_unique<UDPServer>(ip,port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}
Common.hpp
#pragma once#include <iostream>#define Die(code) \do \{ \exit(code); \} while (0)#define CONV(v) (struct sockaddr *)(v)enum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR
};