【Linux】揭开套接字编程的神秘面纱(上)
🌠 作者:@阿亮joy. 🎆专栏:《学会Linux》 🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
👉预备知识👈源IP地址和目的IP地址端口号PID和端口号的区别套接字认识UDP协议认识TCP协议网络字节序
👉套接字编程👈套接字的分类套接字的数据结构socket常见API
👉UDP服务器👈echo服务器指令服务器简易的公共聊天室
👉总结👈
👉预备知识👈
源IP地址和目的IP地址
源 IP 地址指发送方的 IP 地址,而目的 IP 地址是指接收方的 IP 地址,源 IP 地址和目的 IP 地址是包含在数据包的IP 头部(IP header)中的,IP 头部是每个 IP 数据包都必须包含的一部分。这两个地址在网络传输过程中是不变的,因为它们是数据包的一部分,并且用于确定数据包的源和目的地。
在网络传输过程中,每个节点都会读取 IP 头部的内容,并基于其中的源 IP 地址和目的 IP 地址等信息来决定下一步的路由和转发操作。有了源 IP 地址和目的 IP 地址,还不能完成网络传输,还需要 MAC 地址。IP 地址用于标识主机在网络中的位置,MAC 地址用于标识主机的网络接口。在不同网络之间的通信,通常需要使用 IP 地址进行路由和转发,而在同一局域网内的通信,则需要使用 MAC地址进行直接传输。
端口号
端口号是传输层协议的内容,它是用于标识网络应用程序的通信端口的一个 16 位的数字,其取值范围是 0 到 65535。其中 0 到 1023 的端口号被保留用于一些特定的服务和应用程序,称为“系统端口”或“熟知端口”,例如 HTTP 服务使用的端口号为 80,SMTP 服务使用的端口号为 25。每个端口号都与一个特定的应用程序或服务相关联,用于区分同一主机上的不同应用程序或在网络上的不同主机上的不同应用程序。
在一个网络数据包中,源和目标主机的 IP 地址用于标识主机在网络中的位置,而源和目标端口号则用于标识主机上的应用程序(进程)。网络应用程序使用端口号来与其他应用程序进行通信。在网络传输中,源主机上的应用程序将数据发送到目标主机上的特定端口号,目标主机会将数据包路由到相应的应用程序上进行处理。因此,端口号的作用是为应用程序提供一种可靠的通信机制,使不同的应用程序可以在同一主机上共存,或者在不同主机上进行通信。
注:IP地址 + 端口号能够标识网络上的某一台主机的某一个进程,一个端口号只能被一个进程占用,而一个进程可以绑定多个端口号。
PID和端口号的区别
PID 可以标识唯一的进程,而端口号也能标识唯一的一个进程。那为什么网络通信不采用 PID 来表示唯一的进程呢?虽然 PID 在某些情况下可以用来标识特定的应用程序(进程),但在网络中,PID 不是一种可靠的方式来标识应用程序,原因如下:
PID 只在单个计算机上是唯一的:每个计算机上的进程都有自己的 PID,因此,在不同的计算机上运行的同一应用程序具有不同的 PID。这意味着在网络上使用 PID 来标识应用程序时,它只能标识单个计算机上的应用程序,而不能唯一地标识整个网络中的应用程序。PID 是动态的:当一个应用程序在计算机上启动时,它被分配一个 PID。但是,当该应用程序终止后,该 PID 将被释放并可以被操作系统重新分配给其他进程。因此,使用 PID 来标识应用程序可能会导致标识符冲突或标识符混淆,因为一个新的应用程序可能会被分配以前已经被释放的 PID。
相比之下,端口号是一种更可靠的方式来标识网络中的应用程序,因为它在整个网络中是唯一的,并且不会被操作系统重新分配给其他应用程序。此外,端口号可以方便地被网络管理人员和安全人员用于限制或控制网络通信,从而增强网络安全性。
套接字
套接字(socket)是一种用于在计算机网络中进行通信的软件设备,它提供了一种抽象层,使得应用程序可以使用统一的接口来进行网络通信,而无需了解底层网络协议的复杂性。
套接字通常由一个 IP 地址和一个端口号组成,它们一起标识了网络中的一个特定的节点。一个套接字可以用来建立连接、发送和接收数据,同时也可以被关闭和释放。
在应用程序中,套接字通常由操作系统提供的套接字库进行管理。套接字库提供了一组函数,使得应用程序可以方便地创建、绑定、监听和连接套接字,并进行数据的发送和接收。套接字库也提供了一些高级函数,如 select 和 poll,用于进行异步通信和多路复用。
套接字可以用于各种类型的网络通信,如 TCP、UDP 和RAW 等协议。TCP 套接字提供了面向连接的、可靠的数据传输服务,适用于需要可靠传输的应用程序,如 Web 浏览器、邮件客户端等;UDP 套接字则提供了无连接、不可靠的数据传输服务,适用于实时性要求高、数据传输量较小的应用程序,如在线游戏、语音聊天等;RAW 套接字则可以让应用程序直接访问网络协议栈,适用于需要自定义协议和进行网络调试的应用程序。
综上,网络编程也被称为套接字编程。
认识UDP协议
UDP 协议(用户数据报协议)是一种无连接的、面向数据报、不可靠的协议,它不提供连接建立和数据校验等功能,而是将数据直接打包成数据报发送,不保证数据的可靠性。UDP 适用于实时性要求较高的应用,如在线游戏、语音聊天等,因为它具有低延迟、高吞吐量的优势。UDP 的优点在于它的简单、高效,但是由于它不保证数据的可靠性,因此需要应用程序自己处理数据的错误和丢失等问题。
认识TCP协议
TCP 协议(传输控制协议)是一种面向连接的、年面向字节流的、可靠的协议,它通过三次握手建立连接,并提供了流控制、拥塞控制、错误校验等功能,保证数据传输的可靠性。TCP 适用于对数据可靠性要求较高的应用,如 Web 浏览器、邮件客户端、文件传输等。TCP 协议的优点在于它可以保证数据的可靠传输,但是由于它的连接建立和数据校验等过程会增加网络传输的延迟和开销,因此在实时性要求较高的应用中并不适用。
总的来说,TCP 适用于对数据可靠性要求较高的应用,UDP 适用于实时性要求较高、数据传输量较小的应用。在实际应用中,根据应用的不同需求,选择合适的协议进行网络传输,也可以根据需要将 TCP 和 UDP 协议结合使用,来达到更好的效果。
网络字节序
网络字节序(Network Byte Order)是一种统一的字节序,用于在计算机网络中进行数据传输。由于不同的计算机可能使用不同的字节序(大小端),因此在网络传输中,需要使用一种固定的字节序来确保数据的正确传输和解析。
网络字节序采用的是大端字节序(Big-endian):将高位字节存放在内存的低地址处,低位字节存放在内存的高地址处。在网络字节序中,所有数据类型(如整型、浮点型、字符型等)都采用相同的字节序,这样就可以保证在不同的计算机上进行数据传输和解析时,不会出现字节序不一致的问题。
在 C 语言中,可以使用 htons、htonl、ntohs、ntohl 等函数来进行字节序转换,其中,htons 和 htonl 函数用于将主机字节序转换为网络字节序,ntohs 和 ntohl 函数用于将网络字节序转换为主机字节序。
总之,网络字节序是一种固定的字节序,用于在计算机网络中进行数据传输和解析,保证了不同计算机之间数据的互通性和正确性。
👉套接字编程👈
套接字的分类
域间套接字(Inter-process Communication Socket,IPC Socket):也叫UNIX域套接字(Unix Domain Socket),是一种特殊的套接字类型,用于在同一台计算机上不同进程之间进行通信,属于进程间通信(IPC)机制的一种。它不需要经过网络协议栈的处理,因此传输速度非常快,也不会受到网络攻击的影响。域间套接字使用一个本地的文件名作为地址,进程可以通过这个地址来建立连接并进行通信。 原始套接字(Raw Socket):也叫原始套接字类型,可以直接访问传输层以下的网络层协议,用于构造和发送自定义的网络协议数据包。原始套接字通常用于网络协议分析、网络攻击、网络安全等领域。使用原始套接字需要有足够的权限,因为它可以直接访问网络层协议,对网络安全带来潜在的威胁。 网络套接字(Internet Socket):也叫基于IP协议的套接字,是使用 TCP / IP 协议族进行通信的套接字类型。网络套接字提供可靠的面向连接的通信服务,支持 TCP 和 UDP 协议。在使用网络套接字时,需要使用 IP 地址和端口号来标识网络中的进程,IP 地址用于标识主机,端口号用于标识主机上的应用程序。网络套接字使用 IPv4 或 IPv6 协议,其中 IPv4 协议使用 32 位地址,IPv6 协议使用 128 位地址,能够更好地满足互联网的需求。
那 Linux 是如何设计这三类套接字的呢?Linux 系统为不同类型的套接字提供了统一的 socket API,并根据不同的地址族和套接字类型实现了不同的网络协议和数据结构。应用程序只需要按照 API 规范来创建和使用套接字,就可以实现进程间通信和网络通信等功能。
套接字的数据结构
sockaddr、sockaddr_in 和 sockaddr_un 都是在 socket 编程中用于表示套接字地址的数据结构,它们之间的关系如下:
sockaddr 是一个通用的套接字地址结构体,它包含了地址族、地址信息等字段。在 socket 编程中,通常需要将sockaddr 类型的地址转换成对应的具体类型的地址结构体,例如 sockaddr_in 或 sockaddr_un,才能方便地进行相关的操作。sockaddr 的结构体定义如下:
struct sockaddr
{
sa_family_t sa_family; //地址族(AF_xxx)
char sa_data[14]; //14字节协议特定地址信息
};
sa_family 表示地址族,具体取值可以是 AF_UNIX、AF_INET、AF_INET6 等,sa_data 字段是协议特定的地址信息。在实际使用中,sockaddr 通常会被转换为其他具体的地址结构体,例如 sockaddr_in 或 sockaddr_un。 sockaddr_in 是 Internet 域套接字地址结构体,它在sockaddr 的基础上增加了 IPv4 地址和端口号字段。sockaddr_in 的结构体定义如下:
struct sockaddr_in
{
sa_family_t sin_family; //地址族(AF_INET)
uint16_t sin_port; //16位端口号
struct in_addr sin_addr; //32位IPv4地址
char sin_zero[8]; //不使用的填充字段
};
sockaddr_un 是 Unix 域套接字地址结构体,它在sockaddr 的基础上增加了一个路径名字段。sockaddr_un的结构体定义如下:
struct sockaddr_un
{
sa_family_t sun_family; //地址族(AF_UNIX)
char sun_path[108]; //文件路径名
};
在实际的 socket 编程中,通常需要根据具体的网络协议和地址族来选择使用合适的套接字地址结构体。例如,如果要使用 IPv4 协议进行通信,就需要使用 sockaddr_in 来表示 IPv4 地址和端口号。如果要实现 Unix 域套接字通信,就需要使用 sockaddr_un 来表示文件路径名。而sockaddr 则可以作为通用的套接字地址结构体,用于一些通用的场景,例如进行套接字地址转换等。
socket常见API
// 创建 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);
Linux 系统中,域间套接字、原始套接字和网络套接字都是通过 socket API 来创建和使用的。
原始套接字:在 Linux 系统中,原始套接字通常需要 root 权限才能创建和使用。创建原始套接字时,应用程序需要调用 socket() 函数,指定地址族参数为 AF_PACKET,类型参数为 SOCK_RAW,然后再调用 bind() 函数将原始套接字绑定到指定的网卡上。通过原始套接字,应用程序可以访问传输层以下的网络层协议,例如IP、ICMP、ARP等。域间套接字:在 Linux 系统中,域间套接字通常被实现为文件系统中的一个文件。创建域间套接字时,应用程序需要调用 socket() 函数,指定地址族参数为 AF_UNIX,类型参数为 SOCK_STREAM 或 SOCK_DGRAM,然后再调用 bind() 函数将文件名绑定到套接字上。通过文件名,进程可以找到对应的域间套接字,进行进程间通信。网络套接字:在 Linux 系统中,网络套接字通常使用 IPv4 或 IPv6 协议进行通信。创建网络套接字时,应用程序需要调用 socket() 函数,指定地址族参数为 AF_INET 或 AF_INET6,类型参数为 SOCK_STREAM 或 SOCK_DGRAM,然后再调用 bind() 函数将套接字绑定到指定的 IP 地址和端口号上。通过网络套接字,应用程序可以实现基于 TCP 或 UDP 协议的网络通信。
👉UDP服务器👈
echo服务器
echo 服务器想要实现的功能是将客户端发送过来的数据,回显给客户端。
recvfrom 函数的最后两个参数的含义:
为什么服务端进行绑定时,建议绑定全零的 IP 地址呢?
UdpServer.hpp
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP
#include "Log.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SIZE 1024
class UdpServer
{
public:
UdpServer(uint16_t port, std::string ip = "")
: _port(port)
, _ip(ip)
{}
void InitServer()
{
// 1. 创建套接字
// 前两个参数已经能确定是是UDP的网络通信了,第三个参数设置为0即可
_socket = socket(AF_INET, SOCK_DGRAM, 0);
if(_socket < 0) // 创建套接字失败
{
logMessage(FATAL, "errno:%d, strerror", errno, strerror(errno));
exit(2);
}
// 2. 绑定端口号(将用户设置的ip和端口号与当前的进程强绑定)
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
// 服务器的IP地址和端口号是要发给对方主机的,首先要发送到网络
local.sin_port = htons(_port);
// 点分十进制的IP地址: "192.168.110.132"
// 每一个区域取值范围是[0-255]: 1字节 -> 4个区域
// 理论上,表示一个IP地址,其实4字节就够了
// 需要将点分十进制字符串风格的IP地址转成4字节的二进制序列
// 4字节的二进制序列,还需要从主机序列转为网络序列
// 以上过程可以通过inet_addr函数来完成
// sin_addr.s_addr是4字节的二进制网络序列
// INADDR_ANY表示发给这台主机上的指定端口的数据都要交给UdpServer
// 如果绑定指定IP,就只能接收发给该IP的数据
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if(bind(_socket, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "errno:%d, strerror", errno, strerror(errno));
exit(3);
}
logMessage(NORMAL, "Init UdpServer Success!");
}
void StartServer()
{
// 网络服务器是常驻进程,永远不会退出,除非挂掉了
char buffer[SIZE];
while(true)
{
struct sockaddr_in peer; // 输出型参数
bzero(&peer, sizeof(peer)); // 将比特位全部置为0
socklen_t len = sizeof(peer); // 输入输出型参数
ssize_t s = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
buffer[s] = '\0';
uint16_t clientPort = ntohs(peer.sin_port);
std::string clientIP = inet_ntoa(peer.sin_addr);
printf("clientIP:%s clientPort:%d# %s\n", clientIP.c_str(), clientPort, buffer);
}
// 写回数据
sendto(_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
}
}
~UdpServer()
{
if(_socket >= 0) close(_socket); // 关闭文件描述符
}
private:
uint16_t _port;
std::string _ip;
int _socket;
};
#endif
功能说明:
UdpServer 的构造函数是指定服务器的 IP 地址和端口号。InitServer 接口的功能是创建套接字和绑定端口号。StartServer 接口的功能是接收客户端的数据并将数据写回给客户端。
UdpServer.cc
#include "UdpServer.hpp"
#include
static void Usage(std::string proc)
{
std::cout << "\nUsage: " << proc << "Port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr ptr(new UdpServer(port));
ptr->InitServer();
ptr->StartServer();
return 0;
}
UdpClient.cc
为什么客户端不能显式绑定端口号呢?
客户端通常不需要显式绑定端口号,因为客户端只需要连接到服务端的指定端口号即可。当客户端向服务端发起连接请求时,操作系统会自动为客户端分配一个随机的空闲端口号,并在连接请求中包含该端口号信息,以便服务端能够返回数据给正确的客户端端口。
这种自动分配端口号的机制称为“临时端口号”或“短暂端口号”,它的使用使得客户端和服务端的通信更加简单和可靠。同时,如果客户端需要绑定特定的端口号,则必须要保证该端口号未被其他应用程序占用,否则会导致连接失败。因此,客户端通常不需要显式绑定端口号。
#include
#include
#include
#include
#include
#include
#include
#include
#include
static void Usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " ServerIP ServerPort" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 1. 创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
std::cerr << "Socket Error!" << std::endl;
exit(2);
}
char buffer[1024];
std::string message;
struct sockaddr_in server;
memset(&server, sizeof(server), 0);
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(atoi(argv[2]));
// 客户端一般不需要显示地绑定端口号,如果客户端绑定了一个固定的
// IP和端口号,那么其他客户端也绑定了这个端口号,这时候客户端就会
// 绑定失败。所以客户端一般不需要显式地绑定端口号,而是让操作系统
// 随机选择一个端口号进行绑定。什么时候进行绑定,当客户端首次发送
// 数据给服务器时,操作系统会自动进行客户端的端口绑定
while(true)
{
std::cout << "Please Enter Your Message: ";
std::getline(std::cin, message);
if(message == "quit") break;
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
// 接收数据:当前的客户端有可能是别的主机的服务端
ssize_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
if(s > 0)
{
buffer[s] = '\0';
std::cout << "server# " << buffer << std::endl;
}
}
close(sock);
return 0;
}
功能测试以及注意事项
netstat 是一个用于显示和分析 Linux 系统网络连接状态的命令行工具。下面是常用的 netstat 参数的含义:
-a 显示所有的网络连接状态,包括 TCP、UDP 和 UNIX域套接字。-t 只显示 TCP 连接状态。-u 只显示 UDP 连接状态。-n 不进行主机名和端口号的解析,使用数字形式来显示地址和端口号。-p 显示与连接相关联的程序名称和进程ID。-e 显示与连接相关联的扩展信息,如 TCP 的 SACK 和 Windows 扩展选项。-c 持续输出网络连接状态,每隔一秒钟更新一次。-l 仅显示监听状态的连接。
使用 netstat 命令时,可以根据具体需要组合使用这些参数,以便查看系统的网络连接状态。例如,使用 netstat -tunap 可以同时显示所有 TCP 和 UDP 连接状态,并显示与每个连接相关联的程序名称和进程 ID。
什么是本地环回?
本地环回(loopback)是一种计算机网络通信的机制,它允许计算机通过一个虚拟的网络接口与自己通信。在 TCP / IP 协议中,本地环回地址被定义为 127.0.0.1,也称为回环地址。
当计算机通过回环地址发送数据包时,操作系统会将这些数据包送回到发送者本身,而不是发送到网络上。这种机制使得计算机可以自我测试和调试,同时也可以用于本地服务的访问和通信。
在网络编程中,本地环回地址可以用来测试和调试客户端和服务器程序。例如,可以将客户端程序连接到回环地址的某个端口上,以模拟连接到远程服务器的情况。同样地,服务器程序也可以监听回环地址上的某个端口,以模拟接收来自远程客户端的请求。
总之,本地环回是一种非常有用的网络通信机制,它使得计算机可以在不涉及真实网络的情况下进行自我测试和调试,以及本地服务的访问和通信。
Linux系统中常用的文件传输工具:rz(收) 和 sz(发),通过 sz 指令就可以将 Linux 下的程序发到 Windows 系统上了,rz 指令可以将 Windows 上的文件传给 Linux系统。
日志功能
#pragma once
#include
#include
#include
#include
#include
// 日志等级
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOGFILE "./ThreadPool.log"
const char* levelMap[] =
{
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
void logMessage(int level, const char* format, ...)
{
// 只有定义了DEBUG_SHOW,才会打印debug信息
// 利用命令行来定义即可,如-D DEBUG_SHOW
#ifndef DEBUG_SHOW
if(level == DEBUG) return;
#endif
char stdBuffer[1024]; // 标准部分
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", levelMap[level], timestamp);
char logBuffer[1024]; // 自定义部分
va_list args; // va_list就是char*的别名
va_start(args, format); // va_start是宏函数,让args指向参数列表的第一个位置
// vprintf(format, args); // 以format形式向显示器上打印参数列表
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args); // va_end将args弄成nullptr
// FILE* fp = fopen(LOGFILE, "a");
printf("%s%s\n", stdBuffer, logBuffer);
// fprintf(fp, "%s%s\n", stdBuffer, logBuffer); // 向文件中写入日志信息
// fclose(fp);
}
Makefile
.PHONY:all
all:UdpClient UdpServer
UdpClient:UdpClient.cc
g++ -o $@ $^ -std=c++11
UdpServer:UdpServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f UdpClient UdpServer
指令服务器
指令服务器要实现的功能就是服务端将客户端发送过来的数据当做 Linux 指令,然后在服务端执行该指令并将指令的执行结果返回给客户端。
要实现指令服务器,我们需要借助 popen 函数。
popen 函数是 C 语言标准库中的一个函数,它可以创建一个进程并与之建立一个管道。该管道可以实现进程之间的通信,父进程可以向子进程发送数据,并读取子进程的输出。popen 函数原型如下:
FILE *popen(const char *command, const char *type);
其中,command 参数是要执行的命令或程序,type 参数是打开的模式,可以是 “r”(只读模式)或 “w”(只写模式)。函数返回一个文件指针,可以像读写文件一样操作管道。
例如,以下代码创建一个进程并向其发送数据,并从管道中读取子进程的输出:
#include
int main()
{
FILE *fp;
char buffer[1024];
fp = popen("ls -l", "r");
if (fp == NULL)
{
printf("Error: Failed to execute command.\n");
return -1;
}
while (fgets(buffer, sizeof(buffer), fp) != NULL)
{
printf("%s", buffer);
}
pclose(fp);
return 0;
}
该程序打开一个名为 ls -l 的进程,并将其标准输出读取到一个缓冲区中,最后输出到屏幕上。需要注意的是,popen 函数可能会存在一些安全隐患,因为在打开进程时会执行一个命令字符串。因此,在使用该函数时需要谨慎,避免输入不受信任的命令字符串。
注:只需要改写 echo 服务器的 StartServer 接口即可。strcaststr 是查找子串的函数,它的查找是忽略大小写的。
void StartServer()
{
// 网络服务器是常驻进程,永远不会退出,除非挂掉了
char buffer[SIZE];
while(true)
{
struct sockaddr_in peer; // 输出型参数
bzero(&peer, sizeof(peer)); // 将比特位全部置为0
socklen_t len = sizeof(peer); // 输入输出型参数
char ret[256];
std::string cmdRet; // 命令的执行结果
ssize_t s = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
buffer[s] = '\0';
// 过滤危险指令
if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr)
{
std::string errMessage = "哎呦,你干嘛!";
std::cout << errMessage << buffer << std::endl;
sendto(_socket, errMessage.c_str(), errMessage.size(), 0, (struct sockaddr*)&peer, len);
continue;
}
// buffer中的内容看做指令
FILE* fp = popen(buffer, "r");
if(fp == nullptr) // 打开文件失败
{
logMessage(ERROR, "errno:%d strerror:%s\n", errno, strerror(errno));
continue;
}
// 从文件中读取指令执行结果
while(fgets(ret, sizeof(ret), fp) != nullptr)
{
cmdRet += ret;
}
fclose(fp);
uint16_t clientPort = ntohs(peer.sin_port);
std::string clientIP = inet_ntoa(peer.sin_addr);
printf("clientIP:%s clientPort:%d# %s\n", clientIP.c_str(), clientPort, buffer);
}
// 将指令的执行结果发送给客户端
sendto(_socket, cmdRet.c_str(), cmdRet.size(), 0, (struct sockaddr*)&peer, len);
}
}
简易的公共聊天室
简易版的公共聊天室的主要功能是将一个用户发的信息同步到其他用户中去,那么这就以为这一个用户既要发信息,也要接收其他用户发的信息,而这两个过程可以通过写线程和读线程来模拟。而服务端则需要将已经向服务端发过消息的用户记录下来,以便后续将一个用户发的消息同步给其他用户。
UdpServer.hpp
class UdpServer
{
public:
void StartServer()
{
// 网络服务器是常驻进程,永远不会退出,除非挂掉了
char buffer[SIZE];
while(true)
{
struct sockaddr_in peer; // 输出型参数
bzero(&peer, sizeof(peer)); // 将比特位全部置为0
socklen_t len = sizeof(peer); // 输入输出型参数
char ret[256];
char key[64];
ssize_t s = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
buffer[s] = '\0';
uint16_t clientPort = ntohs(peer.sin_port);
std::string clientIP = inet_ntoa(peer.sin_addr);
// key是用户的IP地址加上端口号
snprintf(key, sizeof(key), "IP:%s Port:%d", clientIP.c_str(), clientPort);
logMessage(NORMAL, "key: %s", key);
// 查找用户是否注册过
auto it = _users.find(key);
// 需要将第一次发消息向服务器的用户保存起来
if(it == _users.end())
{
logMessage(NORMAL, "Add A New User: %s", key);
_users[key] = peer;
}
}
// 给所有的用户发送该消息
for(auto& it : _users)
{
std::string sendMessage = key;
sendMessage += "# ";
sendMessage += buffer;
logMessage(NORMAL, "Push A Message To All Users");
sendto(_socket, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr*)&(it.second), sizeof(it.second));
}
}
}
private:
std::unordered_map _users;
};
Thread.hpp
#pragma once
#include
#include
#include
typedef void*(*func_t)(void*);
class ThreadData
{
public:
void *_args; // 线程执行例程的参数
std::string _name; // 线程名
pthread_t _tid; // 线程ID
};
class Thread
{
public:
Thread(int num, func_t callBack, void* args)
: _func(callBack)
{
char nameBuffer[64];
snprintf(nameBuffer, sizeof nameBuffer, "Thread %d", num);
_data._args = args;
_data._name = nameBuffer;
}
// 创建线程
void Create()
{
pthread_create(&_data._tid, nullptr, _func, (void*)&_data);
}
// 等待线程
void Join()
{
pthread_join(_data._tid, nullptr);
}
// 返回线程的名字
std::string Name()
{
return _data._name;
}
~Thread()
{}
private:
func_t _func; // 线程的执行例程
ThreadData _data; // 线程的属性
};
注:Thread.hpp 是对线程进行了封装,详细介绍可以参考这篇博客:线程池的实现。
UdpClient.cc
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Thread.hpp"
uint16_t ServerPort = 0;
std::string ServerIP;
static void Usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " ServerIP ServerPort" << std::endl;
}
// 写线程
static void* UdpSend(void* args)
{
ThreadData* td = (ThreadData*)args;
int sock = *(int*)td->_args;
std::string message;
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ServerIP.c_str());
server.sin_port = htons(ServerPort);
while(true)
{
std::cerr << "Please Enter Your Message: ";
std::getline(std::cin, message);
if(message == "quit") break;
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
}
return nullptr;
}
// 读线程
static void* UdpRecv(void* args)
{
ThreadData* td = (ThreadData*)args;
int sock = *(int*)td->_args;
std::string threadName = td->_name;
char buffer[1024];
while(true)
{
memset(buffer, sizeof buffer, 0);
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
if(s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}
}
return nullptr;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 1. 创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
// 细节:使用cerr标准错误,这样可以为后面的重定向做准备
std::cerr << "Socket Error!" << std::endl;
exit(2);
}
ServerIP = argv[1];
ServerPort = atoi(argv[2]);
// 读线程和写线程都不会修改端口号,所以不需要进行加锁保护
// 读写进程使用的是同一个端口号sock,sock是文件描述符
// 说明Udp是全双工的(可以同时进行读写且不受干扰)
std::unique_ptr Sender(new Thread(1, UdpSend, (void*)&sock));
std::unique_ptr Recver(new Thread(2, UdpRecv, (void*)&sock));
// 创建写线程和读线程
Sender->Create();
Recver->Create();
// 等待写线程和读线程
Sender->Join();
Recver->Join();
close(sock);
return 0;
}
注:mkfifo 创建命名管道,并将用户接收到的信息重定向到管道文件中,然后用另一个会话从管道文件中读取该用户收到的信息。
什么是全双工通信方式?为什么 Udp 协议是全双工的?
全双工通信是指在通信的双方可以同时进行发送和接收数据的通信方式。这种通信方式可以实现同时双方交换信息,从而提高通信效率。
UDP协议是一个无连接的、面向数据报的协议,同时它也是一种全双工的通信方式,即在同一时刻,它允许数据的发送和接收。
Windows 版本的客户端
#include
#include
#include
#include
#include
#include
using namespace std;
#pragma comment(lib,"ws2_32.lib") //固定用法
uint16_t serverport = 8080;
// serverip需要替换成自己的云服务器的公网IP
std::string serverip = "xxx.xxx.xxx.xxx";
void Sender(SOCKET clientSocket)
{
sockaddr_in dstAddr;
dstAddr.sin_family = AF_INET;
dstAddr.sin_port = htons(serverport);
dstAddr.sin_addr.S_un.S_addr = inet_addr(serverip.c_str());
while (true)
{
std::string message;
std::cerr << "请输入# ";
std::getline(std::cin, message);
if (message == "quit") break;
sendto(clientSocket, message.c_str(), (int)message.size(), 0, (sockaddr*)&dstAddr, sizeof(dstAddr));
message.clear();
}
}
void Recver(SOCKET clientSocket)
{
char buffer[1024];
while (true)
{
memset(buffer, sizeof buffer, 0);
struct sockaddr_in temp;
int len = sizeof(temp);
int s = recvfrom(clientSocket, buffer, sizeof buffer, 0, (sockaddr*)&temp, &len);
if (s > 0)
{
buffer[s] = '\0';
cout << "server echo# " << buffer << endl;
}
}
}
int main()
{
// windows 独有的
WSADATA WSAData;
WORD sockVersion = MAKEWORD(2, 2);
if (WSAStartup(sockVersion, &WSAData) != 0)
return 0;
SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
if (INVALID_SOCKET == clientSocket)
{
cout << "socket error!";
return 0;
}
unique_ptr send(new thread(Sender, clientSocket));
unique_ptr recv(new thread(Recver, clientSocket));
send->join();
recv->join();
// windows 独有
closesocket(clientSocket);
WSACleanup();
return 0;
}
👉总结👈
本篇博客主要讲解了什么是源IP地址和目的IP地址、什么是端口号、PID和端口号的区别、什么是套接字、简单认识UDP和TCP协议、什么是网络字节序、套接字的分类、数据结构和常见API以及使用UDP协议来进行编写服务端和客户端等。以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!💖💝❣️