一、UDP和TCP協議
TCP?(Transmission Control Protocol?傳輸控制協議)的特點:
- 傳輸層協議
- 有連接(在正式通信前要先建立連接)
- 可靠傳輸(在內部幫我們做可靠傳輸工作)
- 面向字節流
UDP?(User Datagram Protocol?用戶數據報協議)的特點:
- 傳輸層協議
- 無連接
- 不可靠傳輸(可能會出現網絡丟包或數據包亂序、重復等問題)
- 面向數據報
?
????????UDP和TCP協議都是隸屬于傳輸層的協議,并且這兩個協議距離用戶來說是最近的。所以一般以數據通信為目的的代碼都是使用的是關于傳輸層提供的這些接口,那在傳輸層提供的協議總共有UDP和TCP兩種協議。
????????其中TCP協議被叫做是傳輸控制協議,并且它的特點是有連接,可靠傳輸,面向字節流這些特點,這些特點會在后續進行討論,而UDP協議是用戶數據包協議,它的特點是無連接,不可靠傳輸,面向數據報。現在只是需要知道的是,TCP協議對于傳輸的內容要進行嚴格的追蹤,必須要確保這個數據包能夠完整的被對方接受了才會善罷甘休,而對于UDP來說卻不是這樣,它只保證自己發送了這個數據即可,至于對方有沒有接受到這個信息不屬于它的關心范圍。
可能你會質疑UDP的傳輸,這是不是也意味著UDP的傳輸就不如TCP呢?為什么還要用UDP?
????????其實這兩個概念都是中性詞,并沒有說到底是哪個協議就好,哪個協議就壞,TCP的傳輸雖然很穩定,不可置疑,但是帶來的問題是追蹤每一個包的相關信息到底有沒有送達就意味著需要消耗額外的資源來進行管理,而對比UDP來說就沒有這些額外的消耗,所以并沒有一個嚴格的定義哪個就比哪個更優,只是在特定的場景可能會略有區分。
二、網絡字節序
首先要清楚大小端的概念:
- 小端:低權值的數放入低地址。
- 大端:低權值的數放入高地址。
我們已經知道,?內存中的多字節數據相對于內存地址有大端和小端之分,?磁盤文件中的多字節數據相對于文件中的偏移地址也有大端小端之分,?網絡數據流同樣有大端小端之分。
如何定義網絡數據流的地址呢?(如果一個大端機用大端的方式發送數據到一個小端機,現在跨網絡我們也不知道數據到底是大端和小端)?在網絡誕生之前,就已經有了大小端的概念了,但是大小端到底誰好誰壞?這其實很難做出一個具體的區分,不同的技術廠商會采取不同的使用方法,但是網絡誕生之后,必須解決的問題是數據到底是用小端來傳輸還是用大端來傳輸,如果不解決這個問題就無法進行適當的網絡傳輸。
那怎么辦呢?最終網絡選擇的一個方法是,不管是大端機器還是小端機器,只要想要在網絡上傳輸,必須傳遞的是大端數據,換句話說大端機器的數據就可以直接在網絡上進行傳輸,但是小端機器的數據就必須要進行合適的轉換才可以,所以也對應的提供了一套接口,來表示把數據進行轉換:
- h 表示 host,n 表示 network,l 表示 32 位長整數,s 表示 16 位短整數。
- 例如 htonl 表示將 32 位的長整數從主機字節序轉換為網絡字節序,例如將 IP 地址轉換后準備發送。
- 如果主機是小端字節序,這些函數將參數做相應的大小端轉換然后返回。
- 如果主機是大端字節序,這些函數不做轉換,將參數原封不動地返回。
三、socket套接字?
socket編程常見的接口
// 創建 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);
// 開始監聽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);
未來在進行寫套接字的時候,一般默認情況下是需要把本主機的ip地址和端口號這樣的套接字信息,通過系統調用來和對應打開的網絡套接字來進行綁定,那么其中網絡套接字也有很多的類型,例如:
- 域間套接字
- 原始套接字
- 網絡套接字
域間套接字:它的側重點更多是同一個機器內,這個域表示的是你的機器本身,在里面進行套接,有點類似于之前管道的概念,通過文件路徑的方式標識一份公共資源,然后再以套接字的方式實現雙方的通信,這個就是域間套接字。其中域間套接字表示的是本地通信。
原始套接字:看做它是一個網絡工具,它一般是繞過傳輸層,使用底層的一些接口來進行開發工具,比如說來進行檢查計算機當前是否聯通,比如要進行抓包等等行為,都是借助原始套接字來進行完成的。
網絡套接字:通常是用來標識用戶之間的網絡通信,也是本篇的重點內容,是指使用TCP或者是UDP的協議來實現了用戶間的數據通信
在這之中有一個需要注意的點,那就是網絡接口的設計者想要做成的效果是,理論上來說未來的不同套接字可能需要三套接口,但是他并不想這樣設計,他想要進行高度抽象出一套共同的接口,來方便進行使用,但是現在的問題是他該如何進行保證網絡接口的統一的呢?接口想要統一,第一個面臨的問題就是參數必須統一,可是該如何解決這個問題呢?
在真實情況下進行網絡通信的時候,使用的結構體里面首先要包含16位的端口號,還有30位的ip地址,還有8位的填充等等,但是如果想用一個接口來實現,就意味著要想辦法克服讓不同的人看到參數后能轉換成自己的資源,那對應的解決方案是,不管是網絡通信還是本地通信,對應的前2個字節,就表明了通信的類型,如下圖所示:
可以看到 sockaddr_in 和 sockaddr_un 是兩個不同的通信場景,區分它們就用 16 地址類型協議家族的標識符。但是,這兩個結構體都不用,我們用 sockaddr。
比方說我們想用網絡通信,雖然參數是 const struct sockaddr *addr,但實際傳遞進去的卻是 sockaddr_in 結構體(注意要強制類型轉換)。在函數內部一視同仁,全部看成 sockaddr 類型,然后根據前兩個字節判斷到底是什么通信類型然后再強轉回去。可以把 sockaddr 看成基類,把 sockaddr_in 和 sockaddr_un 看成派生類,構成了多態體系。
- IPv4 和 IPv6 的地址格式定義在 netinet/in.h 中,IPv4 地址用 sockaddr_in 結構體表示,包括 16 位地址類型,16 位端口號和 32 位 IP 地址。
- IPv4、IPv6 地址類型分別定義為常數 AF_INET、AF_INET6。這樣,只要取得某種 sockaddr 結構體的首地址,不需要知道具體是哪種類型的 sockaddr 結構體,就可以根據地址類型字段確定結構體中的內容。
- socket API 可以都用 struct sockaddr * 類型表示, 在使用的時候需要強制轉化成 sockaddr_in,這樣的好處是程序的通用性,可以接收 IPv4,IPv6,以及 UNIX Domain Socket 各種類型的 sockaddr 結構體指針做為參數。
每一種通信結構體的前面部分都是一樣的,這也就意味著當需要進行匹配的時候,會首先匹配一下前兩個字節,看前兩個字節是哪種結構體的,進而就可以進行區分開了,所以最終,我們對應的網絡套接字在使用的時候需要進行對應的強轉,轉換成所需要的具體的結構體就可以了,有點類似于void的概念,不過由于當時還沒有出現void的概念,所以也就沿用至今了,在使用的時候直接看成是void*來使用就沒有什么使用壓力了。
sockaddr 結構
sockaddr_in 結構
?
雖然 ?socket api ?的接口是 ?sockaddr, 但是我們真正在基于 ?IPv4 ?編程時, 使用的數據結構是 ?sockaddr_in, 這個結構里主要有三部分信息: 地址類型, 端口號, IP ?地址。
in_addr?結構?
四、UDP網絡編碼?
這個文件的主要作用就是可以打印一些日志信息,用來方便編程測試代碼:
// Log.hpp
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen;path = "./log/";}void Enable(int method){printMethod = method;}std::string levelToString(int level){switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}void printLog(int level, const std::string &logtxt){switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"if (fd < 0)return;write(fd, logtxt.c_str(), logtxt.size());close(fd);}void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"printOneFile(filename, logtxt);}~Log(){}void operator()(int level, const char *format, ...){time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);printLog(level, logtxt);}private:int printMethod;std::string path;
};
// udpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
using func_t = std::function<std::string(const std::string &)>;
using namespace std;enum
{SOCKET_ERROR = 1,BIND_ERROR
};uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
const int size = 1024;
Log lg;class UdpServer
{
public:UdpServer(const uint16_t &port = defaultport, const string &ip = defaultip): _sockfd(0), _port(port), _ip(ip), _isrunning(false){}void Init(){// 1. 創建UDP socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){lg(Fatal, "socket create error, sockfd: %d", _sockfd);exit(SOCKET_ERROR);}lg(Info, "socket create success, sockfd: %d", _sockfd);// 2. 綁定socketstruct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());if (bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERROR);}lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));}void Run() // 對代碼進行分層{_isrunning = true;char inbuffer[size];while (_isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&client, &len);if (n < 0){lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = 0;string info = inbuffer;string echo_string = "sever echo#" + info;cout << echo_string << endl;sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const sockaddr *)&client, len);}}~UdpServer(){if (_sockfd > 0)close(_sockfd);}private:int _sockfd;string _ip;uint16_t _port;bool _isrunning;
};
// udpclient.cc
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(server);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socker error" << endl;return 1;}string message;char buffer[1024];while (true){cout << "Please Enter@ ";getline(cin, message);std::cout << message << std::endl;// 1. 數據 2. 給誰發sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;cout << buffer << endl;}}close(sockfd);return 0;
}
// main.cc
#include "UdpServer.hpp"
#include <memory>
using namespace std;void Usage(string proc)
{cout << "\n\rUsage: " << proc << " port[1024+]\n"<< endl;
}string ExcuteCommand(const std::string &cmd)
{FILE *fp = popen(cmd.c_str(), "r");if (nullptr == fp){perror("popen");return "error";}std::string result;char buffer[4096];while (true){char *ok = fgets(buffer, sizeof(buffer), fp);if (ok == nullptr)break;result += buffer;}pclose(fp);return result;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init(/**/);svr->Run();return 0;
}