1.介绍函数使用
1.创建套接字
int socket(int domain, int type, int protocol);
-
domain
:指定协议族,如AF_INET
(IPv4)或AF_INET6
(IPv6)。 -
type
:指定套接字类型,如SOCK_DGRAM
(UDP)或SOCK_STREAM
(TCP)。 -
protocol
:通常设置为0,表示使用默认协议。
返回值:成功时返回一个套接字描述符,失败时返回-1。
2.绑定套接字到一个特定的IP地址和端口号
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
sockfd
:由socket()
返回的套接字描述符。 -
addr
:指向sockaddr
结构的指针,包含要绑定的地址信息。 -
addrlen
:addr
的长度。
返回值:成功时返回0,失败时返回-1。
3.从UDP套接字接收数据,获取发送方地址
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
的指针,用于指定src_addr
的长度。
4.向指定的地址发送数据
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
:dest_addr
的长度。
5.设置套接字选项
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
-
sockfd
:套接字描述符。 -
level
:选项级别,如SOL_SOCKET
。 -
optname
:选项名称,如SO_REUSEADDR
或SO_REUSEPORT
。 -
optval
:指向选项值的指针。 -
optlen
:选项值的长度。
返回值:成功时返回0,失败时返回-1。
6.将网络字节序的IP地址转换为点分十进制
char *inet_ntoa(struct in_addr in);
hostshort
:主机字节序的无符号短整数
返回值:网络字节序的无符号短整数。
7.将主机字节序的无符号整数转换为网络字节序
uint16_t htons(uint16_t hostshort);
-
hostshort
:主机字节序的无符号短整数。
返回值:网络字节序的无符号短整数。
8.将网络字节序的无符号整数转换为主机字节序
uint16_t ntohs(uint16_t netshort);
-
netshort
:网络字节序的无符号短整数。
netstate指令查看状态
etstat
命令是一个功能强大的网络工具,用于查看和分析系统的网络状态。以下是netstat
命令的一些常用选项和示例,帮助你查看网络连接状态:
常用选项
-
-a
:显示所有活动的网络连接,包括监听和非监听状态。 -
-t
:仅显示TCP协议相关的连接。 -
-u
:仅显示UDP协议相关的连接。 -
-n
:以数字形式显示地址和端口号,避免进行DNS解析。 -
-l
:仅显示监听状态的连接。 -
-p
:显示与网络连接相关联的进程ID和程序名称。 -
-r
:显示路由表信息。 -
-i
:显示网络接口统计信息。
类比理解:
bind
就像「分配电话号码」假设你的程序是一个「电话」,网络通信需要两件事:
- 电话号码(IP地址 + 端口):别人通过这个号码找到你。
- 电话机(套接字):用来接听和拨打电话的工具。
bind
的作用:
把你的「电话机」绑定到一个「电话号码」上。这样,别人(其他程序)才能通过这个号码联系到你。
2.实现socket通信
1.服务端构造函数
要接收一个端口号,以及一个处理信息的函数,然后isrunning是一个判断执行状态,true表示运行中,就可以执行主体代码。
UdpServer(uint16_t port,func_t func)
:_sockfd(defaultfd),
_port(port),
_isrunning(false),
_func(func)
{}
2.服务端初始化函数
创建端口号,设定为UDP模式(虽然第三参数为0,但是前两个参数就决定了模式是UDP的),setsockopt函数用来这个原因是,当服务器主动关闭连接的时候,套接字会进入TIME_WAIT状态,这个原因是,当服务器主动关闭连接的时候,套接字会进入TIME_WAIT状态,可以设置一下地址端口重用,不然就会打印出日志bind error信息。bero函数是吧给定的空间进行初始化为0,接着就是初始化协议族为四字节,然后端口号要转成网络字节序,然后设置套接字绑定的ip地址为INADDR_ANY,表示该套接字将监听所有可用的网络接口上的链接请求,INADDR_ANY是一个常量,,表示任何可用的网络接口,这样子设置是为了把所有的客户端信息接收了,客户端可能是各种网络的,所以设置这个可用全部接收,接着就是绑定套接字了,因为是在栈区上创建的,是在用户层面上,需要到内核态里,就需要bind函数来进行绑定,绑定后的套接字才是有用的。
void Init()
{
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd<0)
{
LOG(LogLevel::FATAL)<<"socket error";
exit(1);
}
int opt = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
LOG(LogLevel::INFO)<<"socket success, sockfd:"<<_sockfd;
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n<0)
{
LOG(LogLevel::FATAL)<<"bind error";
exit(2);
}
LOG(LogLevel::INFO)<<"bind success,sockfd:"<<_sockfd;
}
3.服务端开始函数
进入这个函数就可以把运行状态设置为true,然后进入循环,设置buffer用来接收消息,创建sockaddr_in类型peer表示客户端的消息(如端口号和IP地址),因为recvfrom函数需要这个参数,这个函数是在UDP套接字接收数据,peer就是一个输出型参数,执行完后,可以从peer里知道是谁发的,就像信封,上面有发件人的信息,需要注意的是参数需要强转成sockaddr类型的,接收成功就进入判断,从peer结构体里可以得到客户端信息(访问成员变量),然后用inet_ntoa将网络字节序转成点分十进制,调用回调函数,把buffer传过去,得到的信息处理再返回,最后执行sendto函数把信息进行发送回去,告诉客户端已经接收到了这个信息。
在使用
recvfrom
函数时,需要将sockaddr_in
强制转换为sockaddr
类型,原因如下:兼容性
sockaddr
是一个通用的套接字地址结构体,用于支持多种协议族。sockaddr_in
是internet环境下套接字的地址形式,专门用于IPv4地址。套接字函数如
bind
、connect
、recvfrom
等设计为使用通用的sockaddr
结构体作为参数,以便能够支持不同的地址族。通过将sockaddr_in
强制转换为sockaddr
,可以确保这些函数能够接受IPv4地址信息,同时保持接口的一致性。灵活性
强制转换允许程序员在需要时传递特定的地址结构体,同时保持函数接口的通用性。这意味着,如果将来需要支持其他类型的地址(如IPv6地址
sockaddr_in6
),只需更改地址结构体的类型和相应的转换,而无需修改函数调用的代码。统一性
sockaddr
和sockaddr_in
的长度都是16个字节,因此可以互相转换。这种统一的内存布局使得在不同地址类型之间进行转换成为可能,而不会导致数据损坏或访问错误的内存区域。
void Start()
{
_isrunning=true;
while(_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
std::cout << "服务器开始接收数据: " << std::endl;
ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s>0)
{
std::cout<<"进入判断"<<std::endl;
int peer_port=ntohs(peer.sin_port);
std::string peer_ip=inet_ntoa(peer.sin_addr);
buffer[s]=0;
std::cout << "buffer: " << buffer << std::endl;
std::string result=_func(buffer);
std::cout << "result: " <<result << std::endl;
sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);
}
}
}
std::string defaulthandler(const std::string& message)
{
std::cout<<"打印"<<std::endl;
std::string hello="hello, ";
hello+=message;
return hello;
}
4.UdpServer.hpp文件总代码
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogModule;
using func_t =std::function<std::string(const std::string&)>;
const int defaultfd=-1;
class UdpServer
{
public:
UdpServer(uint16_t port,func_t func)
:_sockfd(defaultfd),
_port(port),
_isrunning(false),
_func(func)
{}
void Init()
{
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd<0)
{
LOG(LogLevel::FATAL)<<"socket error";
exit(1);
}
int opt = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
LOG(LogLevel::INFO)<<"socket success, sockfd:"<<_sockfd;
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n<0)
{
LOG(LogLevel::FATAL)<<"bind error";
exit(2);
}
LOG(LogLevel::INFO)<<"bind success,sockfd:"<<_sockfd;
}
void Start()
{
_isrunning=true;
while(_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
std::cout << "服务器开始接收数据: " << std::endl;
ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s>0)
{
std::cout<<"进入判断"<<std::endl;
int peer_port=ntohs(peer.sin_port);
std::string peer_ip=inet_ntoa(peer.sin_addr);
buffer[s]=0;
std::cout << "buffer: " << buffer << std::endl;
std::string result=_func(buffer);
std::cout << "result: " <<result << std::endl;
sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
bool _isrunning;
func_t _func;
};
5.实例实现
main要写命令行参数,要接收端口号,不用IP地址是因为前面设置0地址为IP地址,就不需要接收这个参数了,创建一个port接收传入的端口号,这里的uint_16是一个重定义的变量,设置日志类型向控状态打印信息,std::unique_ptr是一个智能指针,用于管理动态分布的对象,它所管理的对象只能有一个所有者,std::make_unique是C++14引入的一个工厂函数,用于简化unique_ptr的创建过程,会调用new操作符来分配空间,返回一个unique_ptr对象。创建对象执行,这里不是.而是->是因为这是一个智能指针,所以需要箭头调用函数。
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include <functional>
std::string defaulthandler(const std::string& message)
{
std::cout<<"打印"<<std::endl;
std::string hello="hello, ";
hello+=message;
return hello;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
std::cerr<<"Usage:"<<argv[0]<<" port"<<std::endl;
return 1;
}
uint16_t port=std::stoi(argv[1]);
Enable_Console_Log_Strategy();
std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port, defaulthandler);
usvr->Init();
usvr->Start();
return 0;
}
6.客户端实现
客服端的话需要传入地址和端口号,创建套接字,以及sockaddr_in类型变量,初始化创建的sockaddr_in结构体的值,进入循环创建string类型的input,要发送的信息,调用getline函数从cin输入流中获取信息放到input中,调用sendto函数向服务端发送消息,套接字,以及发送内容以及大小,还有向谁发送信息,以及信息大小这些参数,然后就是创建buffer来接收消息,向服务端发送消息,服务端反馈后,客服端接收,接收到就打印出接收的消息。
为什么需要传递
sizeof(server)
指定地址结构体的大小:
addrlen
参数用于告诉sendto
函数目标地址结构体的大小。这对于底层网络库正确解析地址信息至关重要。兼容性:
sockaddr
是一个通用的地址结构体,而sockaddr_in
是专门用于IPv4的地址结构体。通过传递sizeof(server)
,可以确保sendto
函数知道它正在处理的是sockaddr_in
结构体的大小。
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
int main(int argc,char* argv[])
{
if(argc!=3)
{
std::cerr<<"Usage:"<<argv[0]<<"server_ip server_port"<<std::endl;
return 1;
}
std::string server_ip=argv[1];
uint16_t server_port=std::stoi(argv[2]);
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
std::cerr<<"socket error"<<std::endl;
return 2;
}
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(server_port);
server.sin_addr.s_addr=inet_addr(server_ip.c_str());
while(true)
{
std::string input;
std::cout<<"Please Enter#";
std::getline(std::cin,input);
std::cout << "客户端发送数据: " << input.c_str() << std::endl;
ssize_t n=sendto(sockfd,input.c_str(),input.size(),0,(struct sockaddr*)&server,sizeof(server));
(void)n;
char buffer[1024];
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
ssize_t m=recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(m>0)
{
buffer[m]=0;
std::cout<<buffer<<std::endl;
}
}
return 0;
}
Makefile文件
.PHONY:all
all:udpclient udpserver
udpclient:UdpClient.cc
g++ -o $@ $^ -std=c++17
udpserver:UdpServer.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f udpclient udpserver