⭐小白苦学IT的博客主页
⭐初学者必看:Linux操作系统入门
⭐代码仓库:Linux代码仓库
❤关注我一起讨论和学习Linux系统
1.前言
网络编程前言
网络编程是连接数字世界的桥梁,它让计算机之间能够交流信息,为我们的生活和工作带来便利。从简单的网页浏览到复杂的分布式系统,网络编程无处不在。
然而,网络编程涉及诸多复杂概念和技术,如IP地址、端口号、Socket、TCP/UDP协议等,需要我们深入学习和掌握。同时,网络环境的复杂性、数据安全性等问题也带来了挑战。
但正是这些挑战,让网络编程充满了无限可能。掌握网络编程技术,我们可以开发出各种创新应用,为人们提供更高效、智能的服务。
本文旨在介绍网络编程的Socket编程接口及其技术,分享实用经验,帮助读者打下坚实的网络编程基础。
1.socket编程接口
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);
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要谈的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同.
- 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结构体指针做为参数。
sockaddr 结构
sockaddr
是一个通用的套接字地址结构,它用于表示各种类型的套接字地址。但是,sockaddr
结构本身并不包含足够的信息来确定地址的类型,因此它通常被更具体的结构(如 sockaddr_in
)所替代。sockaddr
结构的主要作用是为不同的地址结构提供一个统一的接口。
- 通用性:
sockaddr
是一个通用的套接字地址结构,设计初衷是为了能够表示各种类型的套接字地址,包括IPv4、IPv6以及其他可能的地址类型。这种通用性使得sockaddr
能够作为许多网络编程函数的参数,如bind
、connect
、recvfrom
、sendto
等,用于指明地址信息。- 扩展性:通过定义
sa_family
字段,sockaddr
能够区分不同类型的地址结构。这使得在未来引入新的地址类型时,不需要修改现有函数的接口,只需定义新的地址结构并设置相应的sa_family
即可。
sockaddr_in 结构
sockaddr_in
是 sockaddr
结构的一个特例,用于表示 IPv4 地址和端口号。它包含了 IP 地址和端口号的信息,以及地址族和协议信息。
- IPv4特化:尽管
sockaddr
具有通用性,但在实际编程中,特别是在处理IPv4地址时,直接使用sockaddr
结构会显得过于复杂和冗余。sockaddr_in
结构是针对IPv4地址设计的,它包含了IPv4地址和端口号等必要信息,并且以更直观和易于操作的方式呈现这些信息。- 便利性:
sockaddr_in
提供了专门的字段来存储IPv4地址(sin_addr
)和端口号(sin_port
),这使得在处理IPv4网络编程任务时更加方便和高效。同时,通过类型转换,sockaddr_in
结构可以很容易地转换为sockaddr
结构,从而与需要sockaddr
参数的函数兼容。
in_addr结构
in_addr
结构用于表示一个 IPv4 地址。它通常与 sockaddr_in
结构一起使用,作为 sin_addr
字段的类型。
在这个结构中,s_addr
是一个无符号长整数,表示 IPv4 地址。在实际使用中,我们通常不会直接操作这个长整数,而是使用诸如 inet_pton
和 inet_ntop
这样的函数来将点分十进制格式的 IP 地址(如 "192.168.1.1")转换为 in_addr
结构,或者将 in_addr
结构转换为点分十进制格式的字符串。
- IPv4地址表示:
in_addr
结构专门用于表示IPv4地址。它通过一个无符号长整数(s_addr
)来存储IPv4地址,这种表示方式在网络编程中非常常见。尽管IPv4地址通常以点分十进制的形式表示(如192.168.1.1),但在内部处理和网络传输时,它们通常被转换为这种整数形式。- 转换方便:
in_addr
结构使得在点分十进制格式和内部整数格式之间转换IPv4地址变得相对简单。通过调用如inet_pton
和inet_ntop
这样的函数,可以轻松实现这两种格式之间的转换,从而方便网络编程中的地址处理。
总结一下就是:
sockaddr
是一个通用的套接字地址结构,用于表示各种类型的地址。sockaddr_in
是sockaddr
的一个特例,用于表示 IPv4 地址和端口号。in_addr
用于表示 IPv4 地址。
这三种结构的存在是为了满足不同网络编程需求和提高编程效率。sockaddr
提供了通用性和扩展性,sockaddr_in
则针对IPv4地址提供了更直观和便利的操作方式,而in_addr
则专门用于表示和转换IPv4地址。在实际编程中,根据具体需求选择合适的结构进行处理,可以提高代码的可读性和可维护性。
2.简单UDP的echo服务器(代码实现)
封装 UdpSocket
UdpServer.hpp
默认ip用 0.0.0.0
端口:8080
对udp服务器进行封装:
#pragma once
#include "Log.hpp"
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<errno.h>
#include<functional>
using func_t = std::function<std::string (const std::string&,uint16_t &,const std::string&)>;
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
enum
{
SOCKET_ERR = 1,
BIND_ERR
};
class UdpServer
{
public:
UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
:_sockfd(-1), _port(port), _ip(ip),_isrunning(false)
{
}
void Init()
{
//1.创建udp socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
log.LogMessage(FATAL, "socket create error,_sockfd: %d", _sockfd);
exit(SOCKET_ERR);
}
log.LogMessage(INFO, "socket create success, _sockfd: %d ", _sockfd);
//2.bind 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()); //1.string->uint32_t 2.uint32_t 必须是网络序列的
//local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(_sockfd,(const struct sockaddr *)&local,sizeof(local))<0)
{
log.LogMessage(FATAL,"bind error , error: %d, error string : %s",errno,strerror(errno));
exit(BIND_ERR);
}
log.LogMessage(INFO,"bind success , error: %d, error string : %s",errno,strerror(errno));
}
void Run(func_t func)
{
_isrunning = true;
char inbuffer[size];
while(_isrunning)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
std::cout<<"server is run!!!"<<std::endl;
ssize_t n = recvfrom(_sockfd,inbuffer,sizeof(inbuffer) - 1,0,(struct sockaddr *)&client,&len);
if(n<0)
{
log.LogMessage(WARNING,"recvfrom error, errno: %d ,errno string : %s",errno,strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
std::string clientip = inet_ntoa(client.sin_addr);
inbuffer[n] = 0;
//充当了一次数据的处理
std::string info = inbuffer;
std::string echo_string = func(info,clientport,clientip);
sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&client,len);
}
}
~UdpServer()
{
if(_sockfd>0) close(_sockfd);
}
private:
int _sockfd;//网络文件描述符
std::string _ip;//字符串类型ip地址
uint16_t _port;//服务器进程的端口号
bool _isrunning;
};
Main.cc
#include"UdpServer.hpp"
#include<memory>
#include<iostream>
#include<cstdio>
#include<vector>
void Usage(std::string proc)
{
std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
std::string Handler(const std::string& str,uint16_t & clientport,const std::string& clientip)
{
std::cout<<"[ ip: "<< clientip<<" port: "<<clientport<<" ]# ";
std::string res = "server get a message: ";
res+=str;
std::cout<<res<<std::endl;
return res;
}
bool SafeCheck(const std::string & cmd)
{
std::vector<std::string> key_word =
{
"rm",
"mv",
"cp",
"kill",
"sudo",
"unlink",
"uninstall",
"yum",
"top"
};
for(auto &word:key_word)
{
auto pos = cmd.find(word);
if(pos!=std::string::npos) return false;
}
return true;
}
std::string ExcuteCommand(const std::string & cmd)
{
if(!SafeCheck(cmd)) return "bad man";
FILE* fp = popen(cmd.c_str(),"r");
if(nullptr == fp)
{
perror("popen error");
return "error";
}
std::string result;
char buffer[4096];
while(true)
{
char * getc = fgets(buffer,sizeof(buffer),fp);
if(nullptr == getc)
{
break;
}
result+=buffer;
}
pclose(fp);
return result;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> svr(new UdpServer(port));
svr->Init();
svr->Run(Handler);
return 0;
}
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;
}
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 系统什么时候给我bind呢?首次发送数据的时候
string message;
char buffer[1024];
while (true)
{
cout << "Please Enter@ ";
getline(cin, message);
cout<<message<<endl;
// std::cout << message << std::endl;
// 1. 数据 2. 给谁发
sendto(sockfd, message.c_str(), message.size(), 0, (const 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;
}
日志类:
#pragma once
#include <iostream>
#include <cstdarg>
#include <ctime>
#include <string>
#include <unistd.h>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
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 TimestampToLocalTime()
{
time_t curr = time(nullptr);
struct tm *currtime = localtime(&curr);
char time_buffer[128];
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
currtime->tm_year + 1900, currtime->tm_mon, currtime->tm_mday, currtime->tm_hour,
currtime->tm_min, currtime->tm_sec);
return time_buffer;
}
void WriteLog(const std::string &levelstr, const std::string &message)
{
switch (style)
{
case Screen:
std::cout << message<<std::endl;
break;
case Onefile:
WriteLogToOnefile("all", message);
break;
case Classfile:
WriteLogToClassfile(levelstr, message);
break;
default:
break;
}
}
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 LogMessage(int level, const char *format, ...) // 类c的日志接口
{
char rightbuffer[1024];
va_list args;
va_start(args, format);
vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);
va_end(args);
char leftbuffer[1024];
std::string curtime = TimestampToLocalTime();
std::string levelstr = LevelToString(level);
std::string idstr = std::to_string(getpid());
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s]",
levelstr.c_str(), curtime.c_str(), idstr.c_str());
std::string logInfo = leftbuffer;
logInfo += rightbuffer;
WriteLog(levelstr, logInfo);
}
~Log() {}
private:
int style;
std::string filename;
};
Log log;
class Conf
{
public:
Conf()
{
log.Enable(Screen);
}
~Conf(){}
};
Conf conf;
Makefile
.PHONY:all
all:udpserver udpclient
udpserver:Main.cc
g++ -o $@ $^ -g -std=c++11
udpclient:UdpClient.cc
g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
rm -rf udpserver udpclient
运行结果:
实现了客户端,服务端双方交互,当然我们这只是简单的进行数据处理,其实还可以通过实现其他功能,这里可以发挥自己的想象去写。
地址转换函数
这里只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址,但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
in_addr转字符串的函数:
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void*addrptr。
关于inet_ntoa
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
#include<stdio.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr=0;
addr2.sin_addr.s_addr=0xffffffff;
char* ptr1 = inet_ntoa(addr1.sin_addr);
char* ptr2 = inet_ntoa(addr2.sin_addr);
printf("ptr1: %s,ptr2: %s\n",ptr1,ptr2);
return 0;
}
运行结果:
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果
- 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
- 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
- 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
- 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;
如果测试如下代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{
struct sockaddr_in *addr = (struct sockaddr_in *)p;
while (1)
{
char *ptr = inet_ntoa(addr->sin_addr);
printf("addr1: %s\n", ptr);
}
return NULL;
}
void *Func2(void *p)
{
struct sockaddr_in *addr = (struct sockaddr_in *)p;
while (1)
{
char *ptr = inet_ntoa(addr->sin_addr);
printf("addr2: %s\n", ptr);
}
return NULL;
}
int main()
{
pthread_t tid1 = 0;
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
pthread_create(&tid1, NULL, Func1, &addr1);
pthread_t tid2 = 0;
pthread_create(&tid2, NULL, Func2, &addr2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
运行结果:
这段代码试图创建两个线程,
Func1
和Func2
,它们分别无限循环地打印两个sockaddr_in
结构的 IP 地址。这两个sockaddr_in
结构,addr1
和addr2
,被初始化为具有特定的sin_addr.s_addr
值。
addr1.sin_addr.s_addr
被初始化为0
,这在 IPv4 地址中通常表示一个未指定的地址,或者说是无效的地址。
addr2.sin_addr.s_addr
被初始化为0xffffffff
,这在 IPv4 地址中通常表示广播地址。然而,代码中有一些需要注意的地方:
- inet_ntoa的静态缓冲区:
inet_ntoa
函数使用静态缓冲区来存储转换后的字符串。这意味着如果两个线程同时调用inet_ntoa
,它们可能会覆盖彼此的缓冲区,导致不可预测的结果。因此,在多线程环境中使用inet_ntoa
是不安全的。- 无限循环:两个线程都包含一个无限循环,这会导致程序永远不会退出,除非被外部因素(如用户终止)中断。
- pthread_join:虽然代码中包含了
pthread_join
调用,但由于线程中的无限循环,这些调用实际上永远不会返回,因此main
函数也永远不会结束。测试这段代码时,你会看到两个线程分别不停地打印出相同的 IP 地址字符串,但由于
inet_ntoa
的问题,这些字符串可能会被互相覆盖,导致输出变得混乱。此外,具体的输出取决于操作系统的具体实现和线程调度的行为。在某些情况下,你可能会看到
addr1
和addr2
交替出现,而在其他情况下,你可能会看到某个地址连续出现多次,然后被另一个地址覆盖。总的来说,这段代码并不是一个好的示例,因为它在多线程环境中不正确地使用了
inet_ntoa
,并且包含了无限循环,这会导致程序行为不可预测且难以管理。如果你需要在多线程环境中处理 IP 地址,建议使用更安全的函数,如
inet_ntop
,并确保正确管理线程的生命周期和同步。