目录
一. 网络地址
1. IP 地址
IP 地址 用于唯一标识和定位网络中不同的主机;
IP 地址分为两种:
-
IPv4 (Internet Protocol version 4): 由网络号和主机号组成, 使用 32 位二进制数表示, 也就是 4 字节的无符号整数, 通常使用点分十进制表示, 例如 192.168.0.1;
网络号: 表示该设备所属的网络;
主机号: 表示该设备在该网络中的编号; -
IPv6 (Internet Protocol version 6): 使用 128 位二进制数表示, 16 字节的大小, 通常使用十六进制数字和冒号表示, 例如: ABCD:EF01:2345:6789:ABCD:EF01:2345:6789;
2. MAC 地址
MAC(Media Access Control), 也称为物理地址或硬件地址, 用于在局域网中唯一标识网络适配器(网卡);
MAC 地址 使用 48 位二进制数表示, 6 字节的大小, 通常使用十六进制数字和冒号表示, 例如: AB:CD:EF:01:23:45;
MAC 地址是数据链路层的一部分, 用于在局域网中寻找目标设备, 将数据包从源设备传输至目标设备;
3. 端口号
端口号(Port) 用于标识同一主机中的不同网络进程;
端口号 使用 16 位二进制数表示, 2 字节的大小, 取值范围为 [0, 65535];
端口号根据传输协议分为 TCP端口和 UDP端口, 不同的传输协议可以使用相同的端口号;
IP 地址 + 端口号 可以标识公网环境下, 唯一的网络进程;
一个进程可以绑定多个端口号, 但一个端口号不允许被多个进程绑定;
4. 传输层协议
TCP(Transmission Control Protocol) 传输控制协议 特点:
- 传输层协议;
- 有连接;
- 可靠传输;
- 面向字节流;
UDP(User Datagram Protocol) 用户数据报协议 特点:
- 传输层协议;
- 无连接;
- 不可靠传输;
- 面向数据报;
5. 网络字节序
由于不同主机的大小端可能不同, 所以TCP/IP 协议规定: 网络中传输的数据, 统一采用大端存储方案, 也就是网络字节序;
并且提供了主机字节序和网络字节序互相转换的相关函数;
#include <arpa/inet.h>
// 主机字节序转网络字节序
uint32_t htonl(uint32_t hostlong); // l 表示32位长整数
uint32_t htons(uint32_t hostshort); // s 表示16位短整数
// 网络字节序转主机字节序
uint32_t ntohl(uint32_t netlong); // l 表示32位长整数
uint32_t ntohs(uint32_t netshort); // s 表示16位短整数
二. socket 套接字
1. socket 常见API
#include <sys/types.h>
#include <sys/socket.h>
// 创建 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);
2. sockaddr 结构
struct sockaddr 结构体是用来表示 IP 地址的标准结构体;
而 socket API是一层抽象的网络编程接口, 适用于各种底层网络协议, 所以 struct sockaddr 结构体就需要适用于各种协议的 IP 地址 的格式;
struct sockaddr 结构体衍生出了两个不同的结构体:
- sockaddr_in 网络套接字, 适用于网络通信;
- sockaddr_un 域间套接字, 适用于域间通信;
通信时, 根据16 位地址类型, 判断通信类型;
三. UDP 网络程序
使用 socket 套接字接口及 UDP 协议的实现简单网络通信, 客户端向服务器发送消息, 服务器接受消息后再转发至所有的客户端, 客户端接受消息并打印, 类似聊天室;
服务器端
框架
Server.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
class UdpServer
{
public:
UdpServer()
{}
void Start()
{}
~UdpServer()
{}
private:
// 网络
int _sockfd;
uint16_t _port;
sockaddr_in _sockaddr;
};
1. 创建套接字
进行网络通信, 首先需要创建套接字, 使用 socket() 函数
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
- domain: 表示套接字的地址族, 常用的有 AF_UNIX(unix), AF_INET(IPv4), AF_INET6(IPv6);
- type: 表示数据的传输方式, SOCK_STREAM(流格式传输) 和 SOCK_DGRAM(数据报传输);
- protocol: 表示传输协议, 常用的有 IPPROTO_TCP 和 IPPTOTO_UDP; 若地址类型和数据传输方式只被一种协议支持, 那么 protocol 可以为 0, 表示自动推导传输协议;
返回值:
- 若成功, 返回套接字(文件描述符); 若失败, 返回 -1;
Server.hpp
这里使用的 UDP 协议, 所以地址族选择 AF_INET, 传输方式选择 SOCK_DGRAM;
#pragma once
#include "Log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
class UdpServer
{
public:
UdpServer()
{
// 创建 socket 文件描述符
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 若创建失败
{
LOG(FATAl, "socket fail");
exit(1);
}
}
private:
// 网络
int _sockfd;
};
2. 绑定 IP 地址和端口号
使用 bind() 函数进行绑定
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
参数:
- sockfd: socket 文件描述符;
- addr: sockaddr 结构体变量的指针, 包含 IP 地址和端口号等内容;
- addrlen: sockaddr 结构体变量的大小;
返回值:
- 若成功, 返回 0; 若失败, 返回 -1;
这里是网络通信, 所以使用 sockaddr_in 结构体, 需包含两个头文件;
#include <netinet/in.h>
#include <arpa/inet.h>
sockaddr_in 结构体
/* 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)];
};
__SOCKADDR_COMMON 是一个宏函数, 使用 C 语言中一个语法 ## (拼接两个字符串);
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;
/* This macro is used to declare the initial common members
of the data types used for socket addresses, `struct sockaddr',
`struct sockaddr_in', `struct sockaddr_un', etc. */
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
16 位地址类型的变量名实际上是 _SOCKADDR_COMMON 传入 sin 参数后, 拼接而成的;
sa_family_t sin_family;
端口号 in_port_t 类型 实际上是一个 2 字节, 16 位的无符号整数, 符合端口号的取值范围 [0, 65535];
typedef unsigned short int __uint16_t;
typedef __uint16_t uint16_t;
typedef uint16_t in_port_t;
IP 地址 in_addr 结构体, 其中包含了一个 32 位无符号整数, 存储 IP 地址;
typedef unsigned int __uint32_t;
typedef __uint32_t uint32_t;
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
填充字段不使用, 一般用 0 填充;
可以使用 bzero 函数, 将变量置 0;
#include <strings.h>
void bzero(void *s, size_t n);
Server.hpp
端口号自行设置;
#pragma once
#include "Log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
class UdpServer
{
public:
UdpServer(uint16_t port = 8888)
:_port(port)
{
// 创建 socket 文件描述符
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 若创建失败
{
LOG(FATAl, "socket fail");
exit(1);
}
// 设置 ip地址 和 端口号
bzero(&_sockaddr, sizeof(_sockaddr));
_sockaddr.sin_family = AF_INET; // 设置 16 位地址类型
_sockaddr.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY 即 0, 表示本机的所有 IP
_sockaddr.sin_port = htons(_port); // 设置端口号, 转网络字节序
// 绑定 socket
int flag = bind(_sockfd, (sockaddr*)&_sockaddr, sizeof(_sockaddr));
if (flag < 0)
{
LOG(FATAl, "bind fail");
exit(1);
}
}
private:
// 网络
int _sockfd; // socket 文件描述符
uint16_t _port; // 端口号
sockaddr_in _sockaddr; // sockaddr 结构
};
INADDR_ANY 即 0.0.0.0, 表示本机的所有 IP;
若主机有多个网卡, IP地址, 绑定某个具体的 IP 地址, 就无法接受其他 IP 地址的数据;
点分十进制的字符串(“0.0.0.0”), 转换为无符号短整数, 可以使用 inet_addr() 函数, 并且此函数在进行转换的同时, 还会将主机序列转换为网络序列;
3. 接受/发送消息
使用 recvfrom() 函数可以获取数据;
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数:
- sockfd: socket 文件描述符;
- buf: 输出型参数, 缓冲区;
- len: 缓冲区大小;
- flags: 读取方式(阻塞/非阻塞);
- src_addr: 输出型参数, 发送数据的客户端地址信息的结构体;
- addrlen: 输入输出型参数, src_addr 结构体大小;
返回值:
- 若成功, 返回实际读取的字节数; 若失败, 返回 -1;
void* Recv()
{
while (1)
{
// 创建缓冲区, 对端结构体;
char buf[128];
sockaddr_in src_addr;
socklen_t len = sizeof(src_addr);
bzero(&src_addr, sizeof(src_addr)); // 置零
int flag = recvfrom(_sockfd, (void*)buf, sizeof(buf)-1, 0, (sockaddr*)&src_addr, &len);
if (flag < 0) // 若失败
{
LOG(FATAl, "recvfrom fail");
continue;
}
/*
数据处理...
*/
}
return 0;
}
使用 sendto() 函数可以发送数据;
#include <sys/types.h>
#include <sys/socket.h>
// 读取信息(TCP/UDP 服务器/客户端)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
- sockfd: socket 文件描述符;
- buf: 缓冲区, 发送的数据;
- len: 缓冲区大小;
- flags: 读取方式(阻塞/非阻塞);
- dest_addr: 接收数据的主机地址信息的结构体;
- addrlen: dest_addr结构体大小;
返回值:
- 若成功, 返回实际发送的字节数; 若失败, 返回 -1;
void* Send()
{
string buf;
sockaddr_in* user;
while (1)
{
/*
获取消息, 接收端的 sockaddr 结构体...
*/
// 发送消息
int flag = sendto(_sockfd, (void*)buf.c_str(), buf.size(), 0, (sockaddr*)user, sizeof(*user));
if (flag < 0) // 若失败
LOG(FATAl, "sendto fail");
}
return 0;
}
4. 加入多线程
服务器端使用两个线程, 分别接收消息和发送消息; 一个堵塞队列, 保证线程的同步和互斥;
在接受消息后, 需保存发送端的 sockaddr 结构体, 并且在堵塞队列中推入 消息;
发送消息时, 从堵塞队列中推出 消息, 发送信息至所有已保存地址的 主机;
Server.hpp
#pragma once
#include "Log.hpp"
#include "RingQueue.hpp"
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unordered_map>
#include <algorithm>
class UdpServer
{
public:
UdpServer(uint16_t port = 8888) // 端口号
:_port(port), _recv(bind(&UdpServer::Recv, this)), _send(bind(&UdpServer::Send, this))
{
// 创建 socket 文件描述符
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 若创建失败
{
LOG(FATAl, "socket fail");
exit(1);
}
// 设置 ip地址 和 端口号
bzero(&_sockaddr, sizeof(_sockaddr));
_sockaddr.sin_family = AF_INET; // 设置 16 位地址类型
_sockaddr.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY 即 0, 表示本机的所有 IP
_sockaddr.sin_port = htons(_port); // 设置端口号, 转网络字节序
// 绑定 socket
int flag = bind(_sockfd, (sockaddr*)&_sockaddr, sizeof(_sockaddr));
if (flag < 0) // 若失败
{
LOG(FATAl, "bind fail");
exit(1);
}
}
void Start()
{
_recv.start();
_send.start();
}
void* Recv()
{
char buf[128];
while (1)
{
// 对端结构体;
sockaddr_in src_addr;
socklen_t len = sizeof(src_addr);
bzero(&src_addr, sizeof(src_addr));
// 接受消息
int flag = recvfrom(_sockfd, (void*)buf, sizeof(buf)-1, 0, (sockaddr*)&src_addr, &len);
if (flag < 0)
{
LOG(FATAl, "recvfrom fail");
continue;
}
buf[flag] = 0;
// cout << buf << endl;
// 存储主机地址
_guard.Lock();
string user = inet_ntoa(src_addr.sin_addr) + to_string(ntohs(src_addr.sin_port));
if (!_map.count(user))
_map[user] = src_addr;
_guard.Unlock();
// 推入 消息
_queue.Push(user+": "+buf);
}
return 0;
}
void* Send()
{
string buf;
while (1)
{
// 获取消息, 接收端的 sockaddr 结构体...
_queue.Pop(buf);
// cout << buf << endl;
vector<sockaddr_in*> users;
_guard.Lock();
for (auto& user:_map)
users.emplace_back(&user.second);
_guard.Unlock();
// 发送消息
for (auto& user:users)
{
int flag = sendto(_sockfd, (void*)buf.c_str(), buf.size(), 0, (sockaddr*)user, sizeof(*user));
if (flag < 0)
LOG(FATAl, "sendto fail");
}
}
return 0;
}
~UdpServer()
{
_recv.join();
_send.join();
close(_sockfd);
LOG(DEBUG, "close");
}
private:
// 线程
unordered_map<string,sockaddr_in> _map; // 存储主机地址
RingQueue<string> _queue; // 堵塞队列
LockGuard _guard;
Thread _recv;
Thread _send;
// 网络
int _sockfd; // socket 文件描述符
uint16_t _port; // 端口号
sockaddr_in _sockaddr; // sockaddr 结构
};
Server.cc
#include "Server.hpp"
int main()
{
UdpServer server;
server.Start();
return 0;
}
客户端
客户端相较于服务器端更简洁;
客户端在发送消息之前需要得知服务器端的地址;
和服务器端不同, 客户端不需要手动绑定 IP 地址与端口号, 避免指定重复的端口号, 操作系统会在首次传输数据时自动 bind, 而服务器的端口不能随意改变;
Client.hpp
#pragma once
#include "Log.hpp"
#include "Thread.hpp"
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
class UdpClient
{
public:
UdpClient(const string& ip, uint16_t port) // ip 和 端口号
:_ip(ip), _port(port), _recv(bind(&UdpClient::Recv, this)), _send(bind(&UdpClient::Send, this))
{
// 创建 socket 文件描述符
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(FATAl, "socket fail");
exit(1);
}
// 保存服务端的 ip地址和端口号
bzero(&_server, sizeof(_server));
_server.sin_family = AF_INET;
_server.sin_addr.s_addr = inet_addr(_ip.c_str());
_server.sin_port = htons(_port);
}
void Start()
{
_recv.start();
_send.start();
}
void* Recv()
{
char buf[128];
while (1)
{
sockaddr_in src_addr;
socklen_t len = sizeof(src_addr);
bzero(&src_addr, sizeof(src_addr));
// 接收消息
int flag = recvfrom(_sockfd, (void*)buf, sizeof(buf)-1, 0, (sockaddr*)&src_addr, &len);
_guard.Lock();
if (flag < 0)
{
LOG(FATAl, "recvfrom fail");
_guard.Unlock();
continue;
}
buf[flag] = 0;
cout << buf << endl;
_guard.Unlock();
}
return 0;
}
void* Send()
{
string buf;
while (1)
{
cin >> buf;
// 发送消息
int flag = sendto(_sockfd, (void*)buf.c_str(), buf.size(), 0, (sockaddr*)&_server, sizeof(_server));
_guard.Lock();
if (flag < 0)
LOG(FATAl, "sendto fail");
_guard.Unlock();
}
return 0;
}
~UdpClient()
{
_recv.join();
_send.join();
close(_sockfd);
LOG(DEBUG, "close");
}
private:
LockGuard _guard;
Thread _recv;
Thread _send;
int _sockfd;
string _ip;
uint16_t _port;
sockaddr_in _server;
};
Client.cc
#include "Client.hpp"
int main(int argc, char* argv[])
{
// 可以在命令行指定
// if (argc != 3)
// LOG(FATAl, "argv fail");
// UdpClient client(argv[1], atoi(argv[2]));
// client.Start();
// 也可以直接设置
string ip = "127.0.0.1";
uint16_t port = 8888;
UdpClient client(ip, port);
client.Start();
return 0;
}