文章目录
- 1. 预备知识
- 1.1 源IP地址和目的IP地址
- 1.2 端口号
- 1.3 套接字初识
- 1.4 tcp协议和udp协议简单认识
- 1.5 网络字节序
- 2. udp程序
- 2.1 创建套接字(socket)的系统调用
- 2.2 bind()
- 2.2.1 初始化一个sockaddr_in结构体
- 2.2.2 inet_addr函数
- 2.2.3 0.0.0.0
- 2.2.4 127.0.0.1
- 2.3 recvfrom 从套接字中接受数据 和 sendto 发送数据
- 2.4 现在的代码
- 2.5 写一下Udpclient.hpp
- 2.6 封装一下服务端处理数据的函数
- 2.7 windows下的客户端
- 2.8 网络聊天室
- 2.8.1 inet_ntoa 函数
- 2.8.2 得到客户端的ip地址和端口号
- 2.8.3 多线程
- 2.8.4 不同终端打印
- 2.9 sockaddr 结构
1. 预备知识
1.1 源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地
源IP地址(Source IP Address)
源IP地址是发送数据包的设备的IP地址。当一个设备需要向网络发送数据时,它会在数据包的头部包含自己的IP地址作为源地址。这个地址告诉网络和目标设备数据包来自哪里。
目标IP地址(Destination IP Address)
目标IP地址是接收数据包的设备的IP地址。当数据包在网络上传输时,它会被发送到目标IP地址指定的设备。目标设备使用这个地址来确定数据包是否为自己所期望接收的。
思考:我们光有IP地址就可以完成通信了嘛?
回答:
仅仅有IP地址并不足以完成网络通信,还需要其他信息来确保数据能够正确、高效地从源头传输到目的地。以下是一些其他所需要的
- MAC地址:在局域网(LAN)层面,数据帧的传输依赖于物理地址,即MAC地址。每个网络接口卡(NIC)都有一个全球唯一的MAC地址。当数据在局域网内传输时,交换机会使用MAC地址来决定将数据帧转发到哪个端口。
- 端口号:IP地址标识了网络中的设备,而端口号则标识了设备上的特定服务或应用程序。例如,HTTP服务通常使用端口80,而HTTPS使用端口443。端口号使得同一设备上的多个服务能够同时运行而不发生冲突。
- 传输协议:网络通信需要选择合适的传输协议,如TCP(传输控制协议)或UDP(用户数据报协议)。TCP提供可靠的、面向连接的通信,而UDP提供快速但不可靠的通信。
在某些情况下,网络通信可以被视为一种特殊的进程间通信,因为它也涉及到数据的交换和进程之间的协调。
1.2 端口号
端口号(port)是传输层协议的内容
- 端口号是一个2字节16位的整数
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
- 一个端口号只能被一个进程占用 ,一个进程可以有多个端口号
1.3 套接字初识
套接字(Socket)是网络编程中一个非常基础的概念,它是一种通信端点。网络中,套接字允许设备上的程序(进程)进行双向通信。套接字是网络通信的构建块,它们为进程提供了一种方式来发送和接收数据。
套接字的组成:
套接字由以下两部分组成:
- IP地址:标识网络上的设备。
- 端口号:标识设备上的特定进程。
因此,一个套接字通常由一个IP地址和一个端口号的组合来唯一标识,例如 (IP地址:端口号)
。
1.4 tcp协议和udp协议简单认识
TCP(Transmission Control Protocol 传输控制协议)
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP(User Datagram Protocol 用户数据报协议)
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
1.5 网络字节序
在网络通信中,数据的字节序是一个重要的考虑因素,因为不同的计算机架构可能使用不同的字节序来存储多字节数据类型。字节序主要有两种:
- 大端序(Big-endian):最高有效字节(MSB)存储在最低的内存地址,最低有效字节(LSB)存储在最高的内存地址。
- 小端序(Little-endian):最低有效字节(LSB)存储在最低的内存地址,最高有效字节(MSB)存储在最高的内存地址。
网络协议规定,所有的多字节数值(如端口号、IP地址等)在网络中传输时都应该使用大端序,这被称为网络字节序。这种规定是为了确保数据在不同架构的计算机之间传输时能够被一致地解释。
当你在一台计算机上设置端口号或其他需要在网络上传输的数值时,你需要确保这些数值是以网络字节序的形式发送出去的。如果你的主机是小端序的,那么直接使用主机上的数值(以主机字节序存储)会导致网络另一端的计算机无法正确解释这些数值,因为它们的字节序是相反的。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序, 这些函数将参数做相应的大小端转换然后返回
- 如果主机是大端字节序, 这些函数不做转换,将参数原封不动地返回。
2. udp程序
2.1 创建套接字(socket)的系统调用
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
参数说明:
domain
:指定通信协议的域,常见的有:AF_INET
:IPv4 网络协议AF_INET6
:IPv6 网络协议AF_UNIX
:Unix 域套接字AF_PACKET
:与网络层直接交互的原始套接字
type
:指定套接字的类型,常见的有:SOCK_STREAM
:提供面向连接、可靠的字节流服务,通常用于 TCP 协议SOCK_DGRAM
:提供无连接的、尽最大努力交付的数据报服务,通常用于 UDP 协议SOCK_RAW
:原始套接字,允许直接访问较低层次的协议SOCK_SEQPACKET
:有序、可靠的数据包服务
protocol
:指定使用的特定协议,通常设置为 0,表示让系统选择一个默认协议。对于SOCK_STREAM
,通常使用 TCP 协议(IPPROTO_TCP
),对于SOCK_DGRAM
,通常使用 UDP 协议(IPPROTO_UDP
)。
返回值:
- 成功时,返回新创建的套接字的文件描述符(一个非负整数)。所以创建套接字类似打开一个文件,指向的是某一个网卡设备
- 失败时,返回
-1
,并设置errno
以指示错误原因。
2.2 bind()
bind
函数是 Linux 系统中用于将一个套接字(socket)绑定到一个特定的网络地址的系统调用。这个地址通常由一个 IP 地址和一个端口号组成。bind
函数定义在 <sys/socket.h>
头文件中。
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
参数说明:
socket
:这是由socket
函数创建的套接字的文件描述符。address
:这是一个指向sockaddr
结构体的指针,该结构体包含了要绑定到套接字的地址信息。对于不同的地址族(如 IPv4、IPv6、Unix 域套接字等),这个结构体的具体内容会有所不同。例如,对于 IPv4 地址,它可能是一个sockaddr_in
结构体。address_len
:这是address
指向的结构体的大小,以字节为单位。这个参数是必须的,因为不同的地址族可能会有不同的结构体大小。
返回值:
- 成功时,返回
0
。 - 失败时,返回
-1
,并设置errno
以指示错误原因。
2.2.1 初始化一个sockaddr_in结构体
在 Linux 系统中,sockaddr_in
结构体用于表示 IPv4 地址和端口号,通常用于与 socket
、bind
、connect
等函数一起使用。sockaddr_in
在头文件#include<netinet/in.h>或#include <arpa/inet.h>
中定义。以下是如何初始化 sockaddr_in
结构体的步骤:
- 定义
sockaddr_in
结构体变量:首先,你需要定义一个sockaddr_in
类型的变量。 - 设置地址族:将
sin_family
成员设置为AF_INET
,表示这是一个 IPv4 地址。 - 设置端口号:将
sin_port
成员设置为端口号,通常需要使用htons
函数将主机字节序转换为网络字节序。 - 设置 IP 地址:将
sin_addr
成员设置为所需的 IPv4 地址,可以使用inet_addr
函数将点分十进制的 IP 地址字符串转换为网络字节序的二进制形式。
2.2.2 inet_addr函数
inet_addr
函数用于将一个点分十进制的 IPv4 地址字符串转换为一个网络字节序的32位整数。这个函数定义在 <arpa/inet.h>
头文件中。
函数原型如下:
in_addr_t inet_addr(const char *cp);
参数说明:
cp
:这是一个指向以空字符结尾的字符串的指针,该字符串包含了一个点分十进制的 IPv4 地址,例如 “192.168.1.1”。
返回值:
- 如果转换成功,
inet_addr
返回一个in_addr_t
类型的值,这是一个适合存储 IPv4 地址的32位无符号整数,且该值以网络字节序表示。 - 如果输入的字符串不是一个有效的 IPv4 地址,
inet_addr
返回一个特殊值INADDR_NONE
。INADDR_NONE
通常定义为 -1,但也可能在不同的系统上有不同的定义。
使用 inet_addr
函数时需要注意的是,它不进行错误检查,如果输入的字符串不是有效的 IPv4 地址,它将返回 INADDR_NONE
。因此,在使用返回值之前,你应该检查它是否等于 INADDR_NONE
。
2.2.3 0.0.0.0
在网络编程中,当你使用 bind
函数将一个套接字绑定到一个地址时,如果你将 IP 地址设置为 0.0.0.0
(在 IPv4 中),这意味着套接字被绑定到了一个特殊的地址,称为“ wildcard address”(通配地址)。它表示本机中所有的IPV4地址。
- 监听所有接口:当你将服务器套接字绑定到
0.0.0.0
时,它将监听所有可用的网络接口上(网卡)的指定端口。这意味着服务器将接受发送到该端口的任何网络接口上的数据。 - IPv4 通配符:在 IPv4 中,
0.0.0.0
用作通配符地址,表示“接受任何 IPv4 地址”。 - 与
INADDR_ANY
的关系:在 C 语言的网络编程中,0.0.0.0
通常通过宏INADDR_ANY
来表示。INADDR_ANY
在<netinet/in.h>
头文件中定义,通常被定义为0x00000000
(即0
)。
2.2.4 127.0.0.1
- 本地接口:
127.0.0.1
指向的是本地计算机。它是一个环回地址,意味着数据包发送到这个地址后,不会离开本地计算机,而是直接被操作系统捕获。 - 用于测试:由于
127.0.0.1
总是指向本地计算机,它经常被用于测试网络应用和服务,而不必担心数据在网络中传输。例如,在开发过程中,你可以使用127.0.0.1
来测试服务器是否正确响应请求,而不需要将数据发送到网络上的其他计算机。 - IPv4地址:
127.0.0.1
是一个IPv4地址。在IPv6中,回环地址是::1
。 - 网络字节序:
127.0.0.1
在二进制中表示为0111 0111 0000 0000 0000 0000 0000 0001
,它在内存中的存储顺序(大端序)和网络字节序是一致的。 - 子网掩码:在使用
127.0.0.1
时,通常不需要指定子网掩码,因为它只用于本地通信。 - 网络库和框架:许多网络编程库和框架默认使用
127.0.0.1
作为本地测试地址。 - 安全性:使用
127.0.0.1
可以提高安全性,因为数据不会在网络上传输,从而减少了被网络监听和攻击的风险。
端口号[0, 1023]都是系统内定的端口号,一般不要绑定这些端口号
上面两步(2.1和2.2)的代码汇总, log
是创建的用来打日志的对象
/* 1.创建udp套接字 */
_socketFd = socket(AF_INET, SOCK_DGRAM, 0);
if(_socketFd == -1) {
// 创建失败
log(FATAL, "创建套接字失败, fd: %d, 原因: %s\n", _socketFd, strerror(errno));
exit(SOCKET_ERR);
}
else {
// 创建成功
log(INFO, "创建套接字成功, fd: %d\n", _socketFd);
/* 2. 将一个套接字绑定到一个特定的网络地址 */
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
in_addr_t ipTmp = inet_addr(_ip.c_str());
if(ipTmp == INADDR_NONE) {
// IP非法
log(FATAL, "IP非法\n");
exit(IP_ERR);
}
// sin_addr是个in_addr结构体类型的变量,里面有个in_addr_t类型的成员s_addr
local.sin_addr.s_addr = ipTmp; // 如果想绑定统配地址,也可以使用INADDR_ANY
// 开始绑定
if(bind(_socketFd, (const struct sockaddr*)&local, sizeof(local)) == -1) {
log(FATAL, "绑定失败, 原因: %s\n", strerror(errno));
exit(BIND_ERR);
}
log(INFO, "绑定成功\n");
}
2.3 recvfrom 从套接字中接受数据 和 sendto 发送数据
recvfrom
函数是在 Linux 系统中用于从套接字接收数据的系统调用。它可以用于不同类型的套接字,包括面向连接的(如 TCP)和无连接的(如 UDP)协议。recvfrom
函数定义在 <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
:这个参数通常设置为0,或者可以设置特定的标志来修改接收操作的行为。例如,MSG_DONTWAIT
可以使recvfrom
调用非阻塞。src_addr
:输出型参数,这是一个可选的指针,指向sockaddr
结构体,用于存储发送方的地址信息。如果不需要发送方的地址信息,可以设置为NULL
。addrlen
:输出型参数,这是一个指向socklen_t
类型的变量的指针,该变量在调用前应该初始化为src_addr
指向的缓冲区的大小。在函数返回时,它将被设置为实际接收到的地址结构体的大小。
返回值:
- 成功时,返回接收到的字节数。如果远端正常关闭了连接,并且没有更多的数据可读,返回0。
- 失败时,返回
-1
,并设置errno
以指示错误原因。
sendto
函数是在 Linux 系统中用于发送数据到套接字的系统调用,特别是用于无连接的协议如 UDP。这个函数定义在 <sys/socket.h>
头文件中。
函数原型如下:
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
:这个参数通常设置为0,或者可以设置特定的标志来修改发送操作的行为。例如,MSG_DONTWAIT
可以使sendto
调用非阻塞。dest_addr
:输入型参数,这是一个指向sockaddr
结构体的指针,该结构体包含了接收方的地址信息。对于不同的地址族(如 IPv4、IPv6、Unix 域套接字等),这个结构体的具体内容会有所不同。addrlen
:输入型参数,这是dest_addr
指向的结构体的大小,以字节为单位。
返回值:
- 成功时,返回发送的字节数。
- 失败时,返回
-1
,并设置errno
以指示错误原因。
sendto
函数通常用于 UDP 套接字,因为 UDP 是无连接的,每次发送数据时都需要指定接收方的地址。对于 TCP 套接字,通常使用 send
函数,它不需要指定接收方地址,因为连接已经建立。
2.4 现在的代码
// udpServer.hpp
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h> // memeset
#include "log.hpp"
using namespace std;
const string defaultIP = "0.0.0.0"; // 默认IP地址
const uint16_t defaultPort = 8080; // 默认端口号
const size_t BUFFERSIZE = 1024;
Log log; // 方便打日志
enum
{
SOCKET_ERR,
BIND_ERR,
};
class UdpServer
{
public:
UdpServer(const uint16_t& port, const string& ip);
~UdpServer();
void init();
void run();
private:
int _socketFd = 0;
string _ip;
uint16_t _port; // 服务器进程端口号
bool _isRunning = false;
};
UdpServer::UdpServer(const uint16_t& port = defaultPort, const string& ip = defaultIP) : _port(port), _ip(ip)
{}
UdpServer::~UdpServer()
{
close(_socketFd);
}
void UdpServer::init()
{
/* 1.创建udp套接字 */
_socketFd = socket(AF_INET, SOCK_DGRAM, 0);
if(_socketFd == -1) {
// 创建失败
log(FATAL, "创建套接字失败, fd: %d, 原因: %s\n", _socketFd, strerror(errno));
exit(SOCKET_ERR);
}
else {
// 创建成功
log(INFO, "创建套接字成功, fd: %d\n", _socketFd);
/* 2. 将一个套接字绑定到一个特定的网络地址 */
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
in_addr_t ipTmp = inet_addr(_ip.c_str());
// sin_addr是个in_addr结构体类型的变量,里面有个in_addr_t类型的成员s_addr
local.sin_addr.s_addr = ipTmp;
// 开始绑定
if(bind(_socketFd, (const struct sockaddr*)&local, sizeof(local)) == -1) {
log(FATAL, "绑定失败, 原因: %s\n", strerror(errno));
exit(BIND_ERR);
}
log(INFO, "绑定成功\n");
}
}
void UdpServer::run()
{
/* 从客户端拿数据,处理后再发给客户端 */
_isRunning = true;
while (_isRunning) {
// 接受数据
sockaddr_in client;
socklen_t len = sizeof(client);
string inBuffer(BUFFERSIZE, 0);
if(recvfrom(_socketFd, (char*)inBuffer.c_str(), inBuffer.size(), 0, (sockaddr*)&client, &len) == -1) {
log(WARNING, "接受数据失败, 原因: %s\n", strerror(errno));
}
cout << inBuffer << '\n';
// 服务端进行数据处理
string echoString = "处理器处理数据。。。: " + inBuffer;
// 发回客户端
if (sendto(_socketFd, echoString.c_str(), echoString.size(), 0, (const sockaddr*)&client, len) == -1) {
log(WARNING, "发送数据失败, 原因: %s\n", strerror(errno));
}
}
}
// main.cc
#include "udpServer.hpp"
void Usage(const char* string)
{
cout << "\n\rUsage: " << string << " port(1024+)\n\n";
}
int main(int argc, char* argv[])
{
if(argc != 2) {
Usage(argv[0]);
} else {
uint16_t port = stoi(argv[1]);
// 使用智能指针管理资源
unique_ptr<UdpServer> svr(new UdpServer(port));
svr->init();
svr->run();
}
return 0;
}
2.5 写一下Udpclient.hpp
注意下面几点:
- 对于像 UDP 这样的无连接协议,客户端不需要建立一个持久的连接。它们只是简单地向服务器发送数据包,而不需要监听任何入站连接或数据。因此,在这种情况下,客户端不需要绑定到一个特定的端口。
- 当创建套接字时,如果没有明确地绑定到一个端口,操作系统会自动为客户端分配一个临时的源端口,这是随机的。这个端口用于发送或接受数据,并且只有在套接字的生命周期内有效。这意味着客户端不需要关心使用哪个端口。减少了冲突
// udpClient.cc
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
using namespace std;
Log log;
enum {
SOCKET_ERR,
};
void Usage(const char* string)
{
cout << "\n\rUsage: " << string << " serverIp serverPort\n\n";
}
int main(int argc, char* argv[])
{
if(argc != 3) {
Usage(argv[0]);
}
// 创建套接字
int socketFd = socket(AF_INET, SOCK_DGRAM, 0);
if(socketFd == -1) {
// 创建失败
log(FATAL, "创建套接字失败, fd: %d, 原因: %s\n", socketFd, strerror(errno));
exit(SOCKET_ERR);
}
// 输入服务端的ip和端口号
string serverIp = argv[1];
uint16_t serverPort = stoi(argv[2]);
// 构建服务器信息
sockaddr_in server;
memset(&server, 0, 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);
string message;
while(true) {
// 发送数据
cout << "@Please enter: ";
getline(cin, message);
sendto(socketFd, message.c_str(), message.size(), 0, (sockaddr*)&server, len);
// 拿到服务端处理后的数据
string buffer(1024, 0);
sockaddr_in tmpServer;
socklen_t tmpLen;
if(recvfrom(socketFd, (char*)buffer.c_str(), buffer.size(), 0, (sockaddr*)&tmpServer, &tmpLen) > 0) {
cout << buffer << endl << endl;
}
}
return 0;
}
服务端处理来自客户端的数据,之后再发给客户端
2.6 封装一下服务端处理数据的函数
// udpServer.hpp
using func_t = function<string(const string &)>;
// ...
void UdpServer::run(func_t fun)
{
/* 从客户端拿数据,处理后再发给客户端 */
// ...
string echoString = fun(inBuffer); // 回调fun函数
}
popen
popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。
这个进程必须由 pclose() 函数关闭,而不是 fclose() 函数。pclose() 函数关闭标准 I/O 流,等待命令执行结束,然后返回 shell 的终止状态。如果 shell 不能被执行,则 pclose()
返回的终止状态与 shell 已执行 exit
一样。
函数原型如下:
FILE *popen(const char *command, const char *type);
参数说明:
command
:要执行的命令字符串。type
:一个字符串,指定管道的方向。它必须是以下两个值之一:"r"
:这意味着你将从子进程的标准输出中读取数据。换句话说,子进程的输出(stdout)将连接到popen
调用进程的标准输入(stdin)。这样,你就可以像从文件中读取数据一样,从子进程的输出中读取数据。"w"
:意味着:父进程将向子进程的标准输入写入数据。子进程的输入(stdin)连接到父进程的输出(stdout)。
返回值:
- 如果成功,
popen
返回一个指向新打开的管道的FILE
指针。 - 如果失败,返回
NULL
。
使用 popen
时,通常需要配合 pclose
函数来关闭管道:
// 检查命令的安全性
bool cmdCheck(const string& s)
{
vector<string> dict = {
"rm",
"mv",
"cp",
"sudo",
"yum",
"unlink",
"uninstall",
"top",
"while"
};
for(auto& e : dict) {
// 如果有关键字中的任意一个,就return false
if(s.find(e) != string::npos) return false;
}
return true;
}
struct Deleter
{
void operator()(FILE* p)
{
if(p != nullptr) pclose(p);
}
};
// 执行命令
string exCmd(const string& s)
{
cout << "Server get a commad: " << s << '\n';
if(cmdCheck(s) == false) return "Bad Cmd!";
// FILE* fp = popen(s.c_str(), "r");
// unique_ptr<FILE, decltype(&pclose)> fp(popen(s.c_str(), "r"), pclose);
// unique_ptr<FILE, function<int(FILE*)>> fp(popen(s.c_str(), "r"), pclose);
// unique_ptr<FILE, function<void(FILE*)>> fp(popen(s.c_str(), "r"), [](FILE* p)->void { if(p) pclose(p); });
unique_ptr<FILE, Deleter> fp(popen(s.c_str(), "r"));
if(fp == nullptr) {
perror("poen: ");
return "Popen Error!";
}
string res;
while(true) {
// 将命令输出的字符串读入到buffer中
char buffer[2*BUFFERSIZE];
char* p = fgets(buffer, sizeof(buffer), fp.get());
if(p != nullptr) res += buffer;
else break;
}
// pclose(fp);
return res;
}
2.7 windows下的客户端
代码差别不大
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <WS2tcpip.h>
#include <string>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
int main()
{
WSAData wsd; // 初始化信息
//启动Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) { /*进行WinSocket的初始化,
windows 初始化socket网络库,申请2,2的版本,windows socket编程必须先初始化。*/
cerr << "WinSocket的初始化失败" << endl;
return -1;
}
// 创建套接字
SOCKET socketFd = socket(AF_INET, SOCK_DGRAM, 0);
if (socketFd == -1) {
// 创建失败
cerr << "创建套接字失败" << endl;
return -2;
}
// 服务端的ip和端口号
string serverIp = "124.70.203.1";
uint16_t serverPort = 9000;
// 构建服务器信息
sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
server.sin_addr.s_addr = inet_addr(serverIp.c_str());
// inet_pton(AF_INET, "127.0.0.1", (void*)&server.sin_addr.s_addr);
string message;
while (true) {
// 发送数据
cout << "Please enter$ ";
getline(cin, message);
sendto(socketFd, message.c_str(), (int)message.size(), 0, (sockaddr*)&server, sizeof(server));
// 拿到服务端处理后的数据
string buffer(1024, 0);
sockaddr_in tmpServer;
int tmpLen = sizeof(tmpServer);
if (recvfrom(socketFd, (char*)buffer.c_str(), (int)buffer.size(), 0, (sockaddr*)&tmpServer, &tmpLen) > 0) {
cout << buffer << endl;
}
}
// 关闭socket连接
closesocket(socketFd);
// 清理
WSACleanup();
return 0;
}
2.8 网络聊天室
2.8.1 inet_ntoa 函数
inet_ntoa
函数是 C 语言标准库中用于将网络字节序的 IPv4 地址转换为点分十进制字符串的函数。这个函数定义在 <arpa/inet.h>
头文件中。
函数原型如下:
char *inet_ntoa(struct in_addr in);
参数说明:
in
:这是一个struct in_addr
类型的参数,它包含了一个网络字节序的 IPv4 地址。
返回值:
inet_ntoa
返回一个指向以 null 结尾的字符的指针,该字符串表示点分十进制的 IPv4 地址。如果转换失败,返回 NULL。
注意点:
- 返回的字符串是只读的,并且存储在静态分配的内存中。因此,多次调用
inet_ntoa
可能会导致不同的调用覆盖之前返回的字符串。 - 由于返回的字符串是指向静态内存的指针,因此不应尝试修改该字符串,也不应在程序的生命周期中依赖该字符串,因为它可能会在后续的调用中被覆盖。
inet_ntoa
函数不执行任何错误检查,如果输入的in_addr
结构体不是有效的 IPv4 地址,返回的字符串将不可靠。
2.8.2 得到客户端的ip地址和端口号
// udpServer.hpp
// ...
using func_t2 = function<string(const string &, const string&, const uint16_t&)>; // 用于网络聊天室
enum {
SOCKET_ERR,
BIND_ERR,
};
class UdpServer
{
public:
UdpServer(const uint16_t& port, const string& ip);
~UdpServer();
void init();
void run(func_t fun);
void run2(func_t2 fun);
private:
int _socketFd = 0;
string _ip;
uint16_t _port; // 服务器进程端口号
bool _isRunning = false;
};
// ...
void UdpServer::run2(func_t2 fun)
{
/* 从客户端拿数据,处理后再发给客户端 */
_isRunning = true;
while (_isRunning) {
// 接受数据
sockaddr_in client;
socklen_t len = sizeof(client);
string inBuffer(BUFFERSIZE, 0);
if(recvfrom(_socketFd, (char*)inBuffer.c_str(), inBuffer.size(), 0, (sockaddr*)&client, &len) == -1) {
log(WARNING, "接受数据失败, 原因: %s\n", strerror(errno));
}
// 拿到客户端的ip和端口号
string ip = inet_ntoa(client.sin_addr);
uint16_t cPort = client.sin_port;
string echoString = fun(inBuffer, ip, cPort); // 回调fun函数, 这里有ip和端口号, 方便标识
// 发回客户端
if (sendto(_socketFd, echoString.c_str(), echoString.size(), 0, (const sockaddr*)&client, len) == -1) {
log(WARNING, "发送数据失败, 原因: %s\n", strerror(errno));
}
}
}
// main.cc
#include "udpServer.hpp"
#include <vector>
// ...
string stringHandler2(const string& s, const string& ip, const uint16_t& port)
{
cout << "From [" << ip << ' ' << port << "]# " << s << endl;
string ret;
// 将所有的字符变成大写
for(auto& e : s) {
ret += toupper(e);
}
return ret;
}
int main(int argc, char* argv[])
{
if(argc != 2) {
Usage(argv[0]);
} else {
uint16_t port = stoi(argv[1]);
// 使用智能指针管理资源
unique_ptr<UdpServer> svr(new UdpServer(port));
svr->init();
svr->run2(stringHandler2);
}
return 0;
}
2.8.3 多线程
// udpServer.hpp
void UdpServer::userOperation(const sockaddr_in& client, const string& ip, const uint16_t& port)
{
/* 如果该用户不在哈希表中,就插入 */
if(_onlineUser.count(ip) == true) return;
_onlineUser.insert({ip, client});
cout << "用户[" << ip << ' ' << port << "]# 已经添加到用户列表中!" << endl;
}
void UdpServer::broadcastUser(const string&info, const string& ip, const uint16_t& port)
{
/* 让所有的在线用户看到同一份消息 */
for(const auto& e : _onlineUser) {
string echoString;
echoString += "[";
echoString += ip;
echoString += " ";
echoString += to_string(port);
echoString += "]# ";
echoString += info;
// 发回客户端
socklen_t len = sizeof(e.second);
if (sendto(_socketFd, echoString.c_str(), echoString.size(), 0, (sockaddr*)(&e.second), len) == -1) {
log(WARNING, "发送数据失败, 原因: %s\n", strerror(errno));
} else {
cout << "客户端发送给:" << e.first <<"消息成功" << endl;
}
}
}
void UdpServer::run3()
{
/* 网络聊天室,从客户端拿到信息,转发给所有人 */
_isRunning = true;
while (_isRunning) {
// 接受数据
sockaddr_in client;
socklen_t len = sizeof(client);
string inBuffer(BUFFERSIZE, 0);
if(recvfrom(_socketFd, (char*)inBuffer.c_str(), inBuffer.size(), 0, (sockaddr*)&client, &len) == -1) {
log(WARNING, "接受数据失败, 原因: %s\n", strerror(errno));
continue;
}
// 拿到客户端的ip和端口号
string ip = inet_ntoa(client.sin_addr);
uint16_t cPort = ntohs(client.sin_port);
// 如果该用户不在哈希表中,就插入
userOperation(client, ip, cPort);
// 服务端打印一下
cout << "用户[" << ip << ' ' << cPort << "]说# " << inBuffer << endl;
// 让所有的在线用户看到同一份消息
broadcastUser(inBuffer, ip, cPort);
}
}
// main.cc
// ...
int main(int argc, char* argv[])
{
if(argc != 3) {
Usage(argv[0]);
}
// 创建套接字
int socketFd = socket(AF_INET, SOCK_DGRAM, 0);
if(socketFd == -1) {
// 创建失败
log(FATAL, "创建套接字失败, fd: %d, 原因: %s\n", socketFd, strerror(errno));
exit(SOCKET_ERR);
}
// 输入服务端的ip和端口号
string serverIp = argv[1];
uint16_t serverPort = stoi(argv[2]);
// 构建服务器信息
sockaddr_in server;
memset(&server, 0, 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);
string message;
// 改成多线程,一个读取,一个发送。在UDP中,一个socket可以同时进行读写操作。
thread t1([&] {
while(true) {
// 发送数据
cout << "@Please enter: ";
getline(cin, message);
sendto(socketFd, message.c_str(), message.size(), 0, (sockaddr*)&server, len);
}
});
thread t2([&] {
while(true) {
// 拿到服务端处理后的数据
string buffer(1024, 0);
sockaddr_in tmpServer;
socklen_t tmpLen;
if(recvfrom(socketFd, (char*)buffer.c_str(), buffer.size(), 0, (sockaddr*)&tmpServer, &tmpLen) > 0) {
lock_guard<mutex> lock(coutMutex);
cout << "Received: " << buffer << endl;
}
}
});
t1.join();
t2.join();
return 0;
}
2.8.4 不同终端打印
dev/pts
路径下有不同终端的编号
写一下测试代码
const char* path = "/dev/pts/0";
int main() {
int fd = open(path, O_WRONLY);
// 检查文件描述符是否有效
if(fd == -1) {
perror("open");
return -1;
}
dup2(fd, 1); // 用该文件描述符替换标准输出
printf("hello world");
close(fd);
return 0;
}
我们让udpClient.hpp
的读端重定向到另一个终端中
// udpClient.hpp
int openTerm()
{
int fd = open(path, O_WRONLY);
// 检查文件描述符是否有效
if(fd == -1) {
perror("open");
return -1;
}
dup2(fd, 2); // 用该文件描述符替换标准错误
close(fd);
return 0;
}
// ...
thread t2([&] {
while(true) {
openTerm(); // 重定向终端,现在使用标准错误输出就是像另一个终端打印
// 拿到服务端处理后的数据
string buffer(1024, 0);
sockaddr_in tmpServer;
socklen_t tmpLen;
if(recvfrom(socketFd, (char*)buffer.c_str(), buffer.size(), 0, (sockaddr*)&tmpServer, &tmpLen) > 0) {
lock_guard<mutex> lock(coutMutex);
cerr << "Received:" << buffer << endl;
}
}
});
另一种做法是不需要写openTerm()
,而是使用命令行直接重定向,因为t2
线程中使用的是cerr
2.9 sockaddr 结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同
- 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结构体指针做为参数