目录
👉🏻socket网络编程常用头文件
在C++中使用socket,你需要包含以下头文件:
-
<sys/socket.h>:这个头文件包含了使用socket所需的系统级别的函数和数据结构的声明。
-
<netinet/in.h>:该头文件包含了定义网络地址结构
struct sockaddr_in
的声明,以及一些网络相关的常量和函数原型。 -
<arpa/inet.h>:这个头文件包含了一些与网络地址转换相关的函数原型,例如
inet_addr()
和inet_ntoa()
。 -
<unistd.h>:这个头文件包含了一些系统调用函数的原型,例如
close()
,exit()
。
在 Windows 平台上,你可能需要包含不同的头文件,例如 <winsock2.h>
和 <ws2tcpip.h>
。
👉🏻socket网络编程预备知识函数
bzero
bzero
函数在标准C库中已经被弃用,它通常用于将一块内存区域清零。在现代C++编程中,推荐使用更安全和更具可移植性的替代方案,如 memset
函数或者使用 C++ 标准库提供的 std::fill
函数。
然而,如果你仍然需要了解 bzero
函数的工作原理,我可以简要介绍一下。
bzero
函数通常的形式是这样的:
void bzero(void *s, size_t n);
它接受两个参数:
s
:指向要清零的内存区域的指针。n
:要清零的字节数。
bzero
函数的作用是将从 s
开始的连续 n
个字节设置为零。它通常用于初始化或清空某些内存区域,例如清空一个字符数组或一个结构体。
以下是一个示例用法:
#include <stdio.h>
#include <strings.h>
int main() {
char buffer[10];
bzero(buffer, sizeof(buffer)); // 将 buffer 中的所有字节清零
// 在清零后,buffer 中的内容应该全是 0
for (int i = 0; i < sizeof(buffer); ++i) {
printf("%d ", buffer[i]);
}
return 0;
}
然而,需要注意的是,bzero
函数已经被标记为废弃,因为它不够安全。在一些情况下,编译器可能会发出警告,建议使用更安全的替代方案。
inet_addr
inet_addr
函数是一个用于将点分十进制的 IPv4 地址转换为网络字节顺序的 32 位二进制整数的函数。它在网络编程中经常用于将 IP 地址转换为网络套接字地址结构中使用的格式。
该函数的原型如下:
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
它接受一个指向以空字符结尾的字符串的指针作为参数,并返回一个 in_addr_t
类型的值,该值表示转换后的 IP 地址。
下面是一个示例用法:
#include <stdio.h>
#include <arpa/inet.h>
int main() {
const char *ip_address_str = "192.168.1.1";
in_addr_t ip_address = inet_addr(ip_address_str);
if (ip_address == INADDR_NONE) {
printf("Invalid IP address\n");
} else {
printf("IP address in network byte order: %u\n", ip_address);
}
return 0;
}
需要注意的是,inet_addr
函数存在一些限制和问题,例如不能处理 IPv4 子网掩码,而且在处理不合法的 IP 地址时可能会返回 INADDR_NONE
。因此,更安全和更强大的替代方案是使用 inet_pton
函数,它能够处理更多的情况并提供更好的错误处理机制。
recvfrom
recvfrom
函数是在进行网络编程时常用的一个函数,它用于从一个已连接的套接字接收数据,或者从未连接的套接字接收数据和地址。具体来说,recvfrom
通常用于在 UDP 套接字中接收数据,因为 UDP 是面向消息的协议,不像 TCP 那样有连接的概念。
该函数的原型如下:
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
它接受以下参数:
sockfd
:套接字文件描述符,指定要接收数据的套接字。buf
:接收数据的缓冲区的指针。len
:缓冲区的大小。flags
:接收操作的标志,通常设置为 0。src_addr
:一个指向sockaddr
结构的指针,用于存储发送方的地址信息。addrlen
:一个指向socklen_t
类型变量的指针,用于存储发送方地址信息的长度。
recvfrom
函数的返回值是接收到的数据的字节数。如果返回值为 -1,则表示出现了错误。
下面是一个简单的示例用法,演示了如何使用 recvfrom
函数接收 UDP 数据:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in servaddr, cliaddr;
socklen_t len = sizeof(cliaddr);
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 绑定地址和端口
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 接收数据
ssize_t n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&cliaddr, &len);
buffer[n] = '\0'; // 在接收到的数据后面添加字符串结束符
printf("Received message: %s\n", buffer);
close(sockfd);
return 0;
}
这个示例创建了一个UDP套接字,并绑定到指定的端口。然后使用 recvfrom
函数接收来自客户端的数据,并将数据打印出来。
ssize_t
是一个有符号整数类型,用于表示某些系统调用(如 read
、write
、recvfrom
等)的返回值或错误码。它通常被定义为 typedef
,在标准库中可以找到类似以下的定义:
typedef long ssize_t;
ssize_t
的长度通常与指针的长度相同,即在 32 位系统上为 4 字节,在 64 位系统上为 8 字节。因为它是有符号类型,所以可以表示负数,通常用于表示字节数或者错误码,以便能够返回错误的情况。
在标准的 POSIX 环境中,ssize_t
的定义在 sys/types.h
头文件中。
sendto
sendto
函数用于通过已连接或未连接的套接字发送数据到指定的目标地址。通常在网络编程中,它被用于在 UDP 套接字中发送数据,因为 UDP 是面向消息的协议,不像 TCP 那样有连接的概念。
该函数的原型如下:
#include <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
:套接字文件描述符,指定要发送数据的套接字。buf
:要发送数据的缓冲区的指针。len
:要发送数据的大小。flags
:发送操作的标志,通常设置为 0。dest_addr
:一个指向sockaddr
结构的指针,用于指定目标地址。addrlen
:目标地址信息的长度。
sendto
函数的返回值是实际发送的数据的字节数。如果返回值为 -1,则表示出现了错误。
下面是一个简单的示例用法,演示了如何使用 sendto
函数发送 UDP 数据:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in servaddr;
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置服务器地址和端口
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 发送数据
const char *message = "Hello, UDP Server!";
ssize_t n = sendto(sockfd, message, strlen(message), 0,
(const struct sockaddr *)&servaddr, sizeof(servaddr));
if (n < 0) {
perror("sendto failed");
exit(EXIT_FAILURE);
}
printf("Message sent successfully.\n");
close(sockfd);
return 0;
}
这个示例创建了一个UDP套接字,并设置了服务器的地址和端口。然后使用 sendto
函数向服务器发送数据。
👉🏻正式开搞——编写服务端代码
Makefile(生成目标文件)
Udpserver:Main.cc
g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
rm -rf Udpserver
Log.hpp(用来打印日志信息)
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
enum
{
Debug = 0,
Info,
Warning,
Error,
Fatal
};
enum
{
Screen = 10,
OneFile,
ClassFile
};
std::string LevelToString(int level)
{
switch (level)
{
case Debug:
return "Debug";
case Info:
return "Info";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "Unknown";
}
}
const int defaultstyle = Screen;
const std::string default_filename = "log.";
const std::string logdir = "log";
class Log
{
public:
Log() : style(defaultstyle), filename(default_filename)
{
mkdir(logdir.c_str(), 0775);
}
void Enable(int sty) //
{
style = sty;
}
std::string TimeStampExLocalTime()
{
time_t currtime = time(nullptr);
struct tm* curr = localtime(&currtime);
char time_buffer[128];
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,
curr->tm_hour, curr->tm_min, curr->tm_sec);
return time_buffer;
}
void WriteLogToOneFile(const std::string& logname, const std::string& message)
{
umask(0);
int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
if (fd < 0) return;
write(fd, message.c_str(), message.size());
close(fd);
// std::ofstream out(logname);
// if (!out.is_open())
// return;
// out.write(message.c_str(), message.size());
// out.close();
}
void WriteLogToClassFile(const std::string& levelstr, const std::string& message)//将内容写入某个文件中
{
std::string logname = logdir;
logname += "/";
logname += filename;
logname += levelstr;
WriteLogToOneFile(logname, message);
}
void WriteLog(const std::string& levelstr, const std::string& message)//决定日志写入的位置:屏幕/单个文件/多个文件
{
switch (style)
{
case Screen:
std::cout << message;
break;
case OneFile:
WriteLogToClassFile("all", message);//单个文件
break;
case ClassFile:
WriteLogToClassFile(levelstr, message);//多个文件
break;
default:
break;
}
}
void LogMessage(int level, const char* format, ...) // 类C的一个日志接口
{
char leftbuffer[1024];
std::string levelstr = LevelToString(level);
std::string currtime = TimeStampExLocalTime();
std::string idstr = std::to_string(getpid());
char rightbuffer[1024];
va_list args; // char *, void *
va_start(args, format);
// args 指向了可变参数部分
vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);
va_end(args); // args = nullptr;
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ",
levelstr.c_str(), currtime.c_str(), idstr.c_str());
std::string loginfo = leftbuffer;
loginfo += rightbuffer;
WriteLog(levelstr, loginfo);
}
// void operator()(int level, const char *format, ...)
// {
// LogMessage(int level, const char *format, ...)
// }
~Log() {}
private:
int style;
std::string filename;
};
Log lg;
class Conf
{
public:
Conf()
{
lg.Enable(Screen);
}
~Conf()
{}
};
Conf conf;
ComErr.hpp(放置错误码)
#pragma once
enum
{
Usage_Err = 1,
Socket_Err,
Bind_Err
};
Main.cc
#include "Udpserver.hpp"
#include "ComErr.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_ip local_port\n" << std::endl;
}
int main(int argc,char* argv[3])
{
if(argc!=3)//如果命令行指令数没有三个
{
Usage(argv[0]);
return Usage_Err;
}
string ip = argv[1];//获取ip
uint16_t port = stoi(argv[2]);//获取端口号
//接下来创建Udpserver的指针
unique_ptr<Udpsever> usvr = std::make_unique<Udpsever>(ip,port);//unique_ptr不支持拷贝构造和赋值操作,因此确保了资源的独占性
//使用make_unique记得添加memory头文件
usvr->Init();
usvr->Start();
return 0;
}
UdpServer.hpp(服务端代码)
#include<iostream>
#include "ComErr.hpp"
#include"Log.hpp"
#include <cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<cerrno>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
const string defaultip = "127.0.0.0";
const uint16_t defaultport = 8888;
class Udpsever
{
public:
Udpsever(Udpsever& udp)=delete;//防止拷贝
Udpsever(const Udpsever& udp)=delete;
const Udpsever& operator=(Udpsever& udp)=delete;
Udpsever(const string& ip = defaultip,const uint16_t& port = defaultport)
:_ip(ip),_port(port)
{}
void Init()
{
//1.创建套接字对象
_sockfd = socket(AF_INET,SOCK_DGRAM,0);//创建一个套接字对象,协议族为IPv4,套接字类型是数据报套接字
if(_sockfd<0)
{
//创建失败则打印错误日志
lg.LogMessage(Fatal,"socket err,%d : %s\n",errno,strerror(errno));//strerror需要包含头文件cstring
exit(Socket_Err);
}
//成功则打印返回的套接字描述符
lg.LogMessage(Info,"sockfd : %d\n",_sockfd);
//上述我们只是创建了一个套接字对象,但是我们还必须将套接字对象与特定的网络信息(ip地址+端口号)绑定,这样以后其它计算机才能根据这个网络信息找到我们然后进行通信
//所以接下来我们要与网络信息绑定,才能实现在网络中进行通信
//我们要用到socket编程中的bind函数:用于将套接字与特定的地址(IP地址和端口号)绑定在一起,以便其他计算机可以找到并与之通信
//2.bind并指定网络信息
//2.1指定网络地址信息
struct sockaddr_in local;//创建一个网络地址信息结构体,接下里初始化地址信息
bzero(&local,sizeof(local));//相当于memset
local.sin_family = AF_INET;//1.初始化协议族
local.sin_port = htons(_port);//2.将16位无符号短整数从主机字节序转换为网络字节序(大端序)
local.sin_addr.s_addr = inet_addr(_ip.c_str());//3.初始化ip地址,这里我们想让_ip变为4字节且是网络字节序,所以使用转换函数inet_addr
//2.2将网络地址信息与socket文件对象进行绑定
//bind函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int bd = bind(_sockfd,(const struct sockaddr *)&local,sizeof(local));
if(bd!=0)
{
lg.LogMessage(Fatal,"bind err :%d %s\n",errno,strerror(errno));
exit(Bind_Err);
}
//至此socket初始化完成
}
void Start()
{
int defaultbuffer = 1024;
char buffer[defaultbuffer];
while(true)//服务器永远不退出
{
//接受信息
struct sockaddr_in peer;//用来存储发送方的地址信息
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer)-1/*再减1是给\0留位置*/,0,(struct sockaddr*)&peer,&len);
if(n!=-1)
{
//说明接受信息成功
//接下来我们将打印发送方的信息
buffer[n] = '\0';
cout<<"Client say : "<<buffer<<endl;
//接下来也发送一些信息回应客户端
char retstr[] = "Got it,processing...";
sendto(_sockfd,retstr,sizeof(retstr),0,(struct sockaddr*)&peer,len);
}
}
}
private:
string _ip;//ip地址
uint16_t _port;//端口号
int _sockfd;//套接字描述符
};
代码写完后,我们运行完成后。
再用netstat命令查看服务器状态,看看是否生成了我们的服务端服务器
可以看到确实出现了。
netstat
netstat
命令用于显示网络状态信息,包括网络连接、路由表、接口统计信息等。在 Linux 系统中,netstat
命令通常用于查看网络连接的状态、路由表、接口信息等,以便网络故障排除、性能监控等任务。
netstat
命令的基本语法如下:
netstat [options]
常用的选项包括:
-a
:显示所有连接和监听端口。-t
:仅显示 TCP 协议的连接。-u
:仅显示 UDP 协议的连接。-n
:以数字形式显示地址和端口号,而不是以主机名和服务名显示。-p
:显示建立连接的进程标识符 PID 和程序名称。-r
:显示路由表信息。-i
:显示网络接口信息。
以下是一些常用的示例用法:
- 显示所有连接和监听端口:
netstat -a
- 仅显示 TCP 连接:
netstat -t
- 仅显示 UDP 连接:
netstat -u
- 以数字形式显示所有连接和监听端口:
netstat -an
- 显示建立连接的进程标识符 PID 和程序名称:
netstat -p
- 显示路由表信息:
netstat -r
- 显示网络接口信息:
netstat -i
除了上述选项之外,还有其他一些选项和参数可供选择,可以通过 man netstat
命令查看 netstat
命令的详细使用手册。