一、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编程常见的接口
cpp
// 创建 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网络编码
这个文件的主要作用就是可以打印一些日志信息,用来方便编程测试代码:
cpp
// 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;
};
cpp
// 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. 绑定socket
struct 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;
};
cpp
// 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;
}
cpp
// 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;
}