文章目录
本篇总结的是对于网络套接字的基本认识
UDP和TCP协议
在谈网络套接字前,必须先对于UDP和TCP这两个协议有一个基本的认识,这两个协议都是隶属于传输层的协议,并且这两个协议距离用户来说是最近的,所以一般以数据通信为目的的代码都是使用的是关于传输层提供的这些接口,那在传输层提供的协议总共有UDP和TCP两种协议
其中TCP协议被叫做是传输控制协议,并且它的特点是有连接,可靠传输,面向字节流这些特点,这些特点会在后续进行讨论,而UDP协议是用户数据包协议,它的特点是无连接,不可靠传输,面向数据报,这里对于这两个概念的理解自然不会很深,只是需要知道的是,TCP协议对于传输的内容要进行严格的追踪,必须要确保这个数据包能够完整的被对方接受了才会善罢甘休,而对于UDP来说却不是这样,它只保证自己发送了这个数据即可,至于对方有没有接受到这个信息不属于它的关心范围
这带来的问题是,可能你会质疑UDP的传输,这是不是也意味着UDP的传输就不如TCP呢?其实这两个概念都是中性词,并没有说到底是哪个协议就好,哪个协议就坏,TCP的传输虽然很稳定,不可置疑,但是带来的问题是追踪每一个包的相关信息到底有没有送达就意味着需要消耗额外的资源来进行管理,而对比UDP来说就没有这些额外的消耗,所以并没有一个严格的定义哪个就比哪个更优,只是在特定的场景可能会略有区分
网络字节序
在说网络字节序的概念之前,首先要回顾一个叫做大小端的概念,这个概念已经是一个很久远的概念了,简单来说就是权重较低的值处于低地址处,这就叫做是小端,反之就是大端
那有了这个概念之后,下一个问题是什么是网络字节序?在网络诞生之前,就已经有了大小端的概念了,但是大小端到底谁好谁坏?这其实很难做出一个具体的区分,不同的技术厂商会采取不同的使用方法,但是网络诞生之后,必须解决的问题是数据到底是用小端来传输还是用大端来传输,如果不解决这个问题就无法进行适当的网络传输
那怎么办呢?最终网络选择的一个方法是,不管是大端机器还是小端机器,只要想要在网络上传输,必须传递的是大端数据,换句话说大端机器的数据就可以直接在网络上进行传输,但是小端机器的数据就必须要进行合适的转换才可以,所以也对应的提供了一套接口,来表示把数据进行转换:
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
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);
上述是在socket编程中经常使用的一些接口,后续对于这些接口会进行详细的解释
套接字
未来在进行写套接字的时候,一般默认情况下是需要把本主机的ip地址和端口号这样的套接字信息,通过系统调用来和对应打开的网络套接字来进行绑定,那么其中网络套接字也有很多的类型,例如:
- 域间套接字
- 原始套接字
- 网络套接字
在这之中他们的侧重点并不同,例如对于域间套接字来说,它的侧重点更多是同一个机器内,这个域表示的是你的机器本身,在里面进行套接,有点类似于之前管道的概念,通过文件路径的方式标识一份公共资源,然后再以套接字的方式实现双方的通信,这个就是域间套接字,其中域间套接字表示的是本地通信。而原始套接字是看做是一个网络工具,它一般是绕过传输层,使用底层的一些接口来进行开发工具,比如说来进行检查计算机当前是否联通,比如要进行抓包等等行为,都是借助原始套接字来进行完成的。网络套接字通常是用来标识用户之间的网络通信,也是本篇的重点内容,是指使用TCP或者是UDP的协议来实现了用户间的数据通信
在这之中有一个需要注意的点,那就是网络接口的设计者想要做成的效果是,理论上来说未来的不同套接字可能需要三套接口,但是他并不想这样设计,他想要进行高度抽象出一套共同的接口,来方便进行使用,但是现在的问题是他该如何进行保证网络接口的统一的呢?接口想要统一,第一个面临的问题就是参数必须统一,可是该如何解决这个问题呢?
在真实情况下进行网络通信的时候,使用的结构体里面首先要包含16位的端口号,还有30位的ip地址,还有8位的填充等等,但是如果想用一个接口来实现,就意味着要想办法克服让不同的人看到参数后能转换成自己的资源,那对应的解决方案是,不管是网络通信还是本地通信,对应的前2个字节,就表明了通信的类型,如下图所示:
每一种通信结构体的前面部分都是一样的,这也就意味着当需要进行匹配的时候,会首先匹配一下前两个字节,看前两个字节是哪种结构体的,进而就可以进行区分开了
所以最终,我们对应的网络套接字在使用的时候需要进行对应的强转,转换成所需要的具体的结构体就可以了,有点类似于void的概念,不过由于当时还没有出现void的概念,所以也就沿用至今了,在使用的时候直接看成是void*来使用就没有什么使用压力了
在谈完了上面的话题之后,就可以正式的对于网络套接字来进行一定的使用了,本篇主要是可以把套接字用起来,在下一篇会基于套接字,来实现一个socket编程使得Linux可以和Windows互通消息,甚至做出来一个类似于聊天室的内容,以加深对于协议传输和socket的理解
为了方便使用,这里引入了一个Log.hpp,这个文件的主要作用就是可以打印一些日志信息,用来方便编程测试代码
// 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;
};
因此现在我们就尝试使用一下网络嵌套字:
首先是创建一下网络嵌套字:
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
基本的使用方法就是如上所示,那在之前的内容中知道,在实际编程的时候,使用的这些接口就都是传输层这边的协议就可以,换而言之,未来所写的代码都是应用层的代码
socket函数的使用也比较简单:
第一个参数表示的是这个套接字创建的一个域,例如说它是属于ipv4还是ipv6等等,从上面的参数中能够看到很多参数,比如还有local communication表示的是本地通信,而这个参数其实就是表示了套接字的类型,针对不同的使用场景可以使用不同的套接字,那我们今天写的代码使用的就是ipv4的协议,所以对应的第一个参数只需要写到AF_INET,表示的是创建的是一个ipv4的网络协议
第二个参数表示的是当前socket的一个类型,函数参数也写了名字是type,如果说第一个参数表明使用的协议是什么或者是使用的域是什么,那么这个第二个参数表示的就是最终使用的套接字类型,是使用的是哪种服务,是对应的面向字节流还是面向数据报的,那我们这里使用的是UDP,也就是面向数据报的,所以对应的额类型就写对应的面向数据报的类型即可
第三个参数表示的是protocol,这个参数目前先不填,后续有需要的时候再对它进行对应的填充即可,所以写成0就可以了
那socket函数的返回值是什么呢?
返回值这句话表示的是,如果成功那么新的一个套接字会被返回,如果失败返回-1,错误码被设置,那这表示的意思就是说,在对应的代码中socket函数对应的返回值依旧是一个文件,只不过是以前的这个struct file结构体指向的是某一个具体的网盘,键盘这样的文件,而这里的这个struct file指向的是最底层的网卡设备,所以相当于是可以用这个套接字来进行发送和接受消息,但是发送和接受消息必须得有一个参数,这个参数就是这个socketfd,类比文件描述符,想要对于文件的操作都需要这个文件描述符,那么同理,想要对于网卡文件的操作都需要这个socketfd的支持
那在创建套接字成功之后,那么就要进行绑定的操作,具体的函数调用如下:
这个绑定的操作,第一个参数就是刚才创建好的套接字,第二个参数是一个结构体,第三个参数是这个结构体的长度,那对于现在的场景来讲,要绑定的是一个网络通信,而网络通信的结构是一个通用的接口,就需要根据前面的2个字节的信息来对于这个结构体进行识别,进而匹配到是什么结构体
而在绑定的时候也要进行绑定对应的端口号和ip地址
所以在今天的这份代码当中,创建的结构体就必须是sockaddr_in这个结构体,之后要对这个结构体的信息进行填充,这样就可以进行对应的绑定信息了
在进行填充前,要先对于内容进行初始化,下面提供一种初始化的接口:
它的功能就是把对应的指针的内容全部初始化为0
// 2. 绑定socket
struct sockaddr_in local;
bzero(&local, sizeof(local));
那么再下一步内容,就是要对于local内的字段进行填充了,那首先要填充前,必须要知道这个结构体中有什么内容呢?
下面所示的是vscode中提示的语法高亮,说明至少我们要填充下面的这些信息
先说sin_family这个参数,这个参数表示的就是当前的这个数据类型,就是在前两个字节中表明自己是一个什么样的数据类型
下一个是这个sin_addr这个参数,这个参数表示的就是一个32位的ip地址,如下所示
下一个是sin_port这个参数,这个参数就很好理解了,其实就是端口号,一个16位的端口号
光了解简单的参数还不够,对于family参数的理解不应仅限于此,因此下面要详细的看这个结构体中的相关信息
下面要对于这个结构体中的详细信息进行展示:
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
现在问题出现了,family字段在哪里呢?为什么在这个结构体中看不到family字段呢?其实这个字段就隐藏在了第一个字段中:
这个字段是一个宏,后面这个内容是一个##的拼接宏,所以这个宏字段可以被解释为:
所以,这样不就正好凑出来了这个变量了吗?
所以,整个结构体的字段我们就都能填充完毕了,于是可以进入下一个问题:
端口号这样写就可以了吗?答案是可以,但是有细节:在之前双方进行通信的时候,对方一定要把消息回复给我这个主机,可是对方如果想要把消息发给我就必须知道我的端口号等信息,这样才能把消息给我发回来,否则是不能进行通信的,那基于这个原因,所以在发送消息的时候,我也必须要把我当前的端口号发过去,这样对方才能接收到我的端口号是多少,才能给我回复消息,那因此得出的一个结论是,端口号这个信息是要在网络中来回进行发送的
所以有上面这样的理论,所以在未来进行数据通信的时候,一个数据包中必然要携带的信息有四点,当前主机的ip地址和端口号,目标主机的ip地址和端口号,起码要把这四个信息都携带上,才能进行数据通信,否则要不然发不过去,要不然发不回来,因此基于这样的理论,所以ip也要向网络中发送,也必须是网络序列的!那当然,这样的过程是不需要我们自己实现的,在库函数中早已给我们准备好了这样的功能:
所以有了上面这么一系列理论,就可以进行绑定了:
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(Fatal, "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));
}
所以至此,UDP服务器的代码就写完了,核心代码其实就是这么多,本质上就是创建套接字,再进行绑定,之后就可以进行收发消息的过程了
下面要做的就是让这个服务器跑起来的过程,想让这个服务器跑起来,可以给它传递一定的函数参数,并且服务器一旦运行起来,必然是周期性,周而复始的运行的
那服务器如何读取到数据呢?下面介绍这个接口:
为什么非要用这个接口呢?难道不能和读取文件一样进行读取呢?此时就不得不谈一下UDP特殊的地方,它特殊就特殊在不能直接read和write,因为read和write这样的接口是面向字节流的,而这个UDP是面向数据报的,所以会引入上面的接口
不过这个接口的使用也是比较简单的,只需要从指定的套接字中读取一个报文,读取到一个传入的缓冲区以及这个缓冲区的长度中,未来就可以把数据存放在这个数据中,而flags是设置阻塞还是非阻塞,默认我们设置为阻塞状态
这都好说,最核心的是它后面的这两个参数,前面我们提到了,收信息必须要知道是谁发来的,因此就必须要解决这个问题,因此后面给出的是两个输出型参数,里面存储的就是是从哪里收到消息的问题
那在接受消息之后,那下一步要做的就是把这个数据再发送回客户端中,那发送回客户端要用的接口是下面的接口:
这个sendto接口和套接字的接口几乎一样,首先是一个fd标识,第二个参数是缓冲区以及长度,然后是标记位,后面是两个参数,表示的是要发送到哪里,发送的长度是什么
void Run(func_t func) // 对代码进行分层
{
_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 = func(info);
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const sockaddr *)&client, len);
}
}
因此,整个系统基本就写完了,那么尝试编译运行一下,通过一个netstat命令来查看具体的运行情况:
此时就会发现,已经顺利的启动起来了,而其中的这个foreign address中全0,表示的是可以接受任何客户端给我发来的消息
下一个要谈论的问题还是回到port端口号的问题上来,一般在绑定端口号的时候,有些端口号是被固定占用的,例如有80就是代表着是https的端口号,所以在0-1023之内的端口号是不能随意绑定的,需要root权限,这也其实意味着这些端口号是和特定的服务向关联的
所以,下面我们编写关于客户端的代码,使得在本篇文章中可以基本上实现一个通信的过程,客户端的代码就很简单了,只是一个简单的运用的过程
下面有一个问题是,客户端需要进行绑定吗?有些说法说是不需要的,其实这样的说法是不对的
客户端确实在进行调用的时候不需要进行显示的绑定,但是也是需要绑定的,客户端有自己的ip和端口,客户端和服务器进行进程通信的时候,服务器如何找到客户端?本质上就是因为客户端有自己的ip和端口号,但是确实在具体落实代码的时候不需要进行绑定,为什么呢?
从上面的实验中可以看到,在一个主机上想要绑定两个端口这是不可以的,这也和我们前面的结论是相同的,不能出现两个进程绑定同一个端口,一个端口只能被一个进程绑定,但是一个进程可以绑定多个端口,这也就意味着如果有人开发恶意的app,在客户端的角度上绑定了很多很多的端口,那么想要运行其他的app的时候,就会出现端口绑定失败的问题,所以这是客户端不用手动绑定端口的原因之一
第二个原因是,假设一个人的手机客户端上有3个app,那app开发的公司在开发客户端的时候该如何进行设定呢?定死了只能绑定某个特定的端口吗?那当其他的app也设定的是绑定这个特定的端口的时候,就会遇见特别严重的问题,这是不被允许的,因此直接把端口绑定这件事交给操作系统来做,让操作系统完成这个过程即可
好了,这下已经完成了基本的通信原理,那么来看下源代码吧
// 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;
};
// 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;
}
标签:socket,int,通信,Linux,sockfd,接字,include,string
From: https://blog.csdn.net/qq_73899585/article/details/136811347