目录
一.创建socket套接字(服务器端)
int socket(int domain, int type, int protocol);
domain:选择你要使用的网络层协议 一般是ipv4,也就是AF_INET
type:选择你要使用的应用层协议,这里我们选择udp,也就是SOCK_DGRAM
protocol:这里我们先设置成0
成功返回文件描述符,失败返回-1
//1.创建socket套接字
_socketfd = socket(AF_INET,SOCK_DGRAM,0);
if(_socketfd < 0)
{
LOG(FATAL, "SOCKET ERROR, %s, %d\n", strerror(errno), errno);
exit(SOCKET_ERROR);
}
二.bind将prot与端口号进行绑定(服务器端)
2.1填充sockaddr_in结构
uint16_t htons(uint16_t hostshort);//将端口号从主机序列转成网络序列
in_addr_t inet_addr(const char *cp);//将ip从主机序列转成网络序列 + 字符串风格ip转成点分十进制ip
uint16_t ntohs(uint16_t netshort);//将端口号从网络序列转成主机序列
char *inet_ntoa(struct in_addr in);//将ip从网络序列转成主机序列 + 点分十进制ip转成字符串风格ip
网络通信:struct sockaddr_in
本地通信:sockaddr_un
16位地址类型表明了他们是网络通信还是本地通信
16位地址类型:sin_family
16位端口号:sin_port
32位ip地址:sin_addr.s_addr
//填充sockaddr_in结构
struct sockaddr_in local;
local.sin_family = AF_INET;//表明是网络通信
local.sin_port = htons(_prot);//将主机序列转成网络序列
local.sin_addr.s_addr = inet_addr("0.0.0.0");//将字符串类型的点分十进制ip转成四字节ip,并转成网络序列
2.2bind绑定端口
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockfd:要绑定的socket套接字的文件描述符
struct sockaddr *:包含ip地址+端口号的结构体(类型不一样需要进行强转)
socklen_t addrlen:sockaddr_in结构体的大小
//bind绑定端口号
int n = bind(_socketfd,(sockaddr *)&local,sizeof(local));//需要将sockaddr_in强转成sockaddr
if(n < 0)
{
LOG(FATAL, "BIND ERROR, %s, %d\n", strerror(errno), errno);
exit(BIND_ERROR);
}
三.直接通信(服务器端)
3.1接收客户端发送的消息
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:数据来自于哪
addrlen:struct sockaddr结构体的大小
//接收客户端发送来的消息
char buffer[1024];
sockaddr_in peer;
socklen_t len = sizeof(peer);
int n = recvfrom(_socketfd,buffer,sizeof(buffer) - 1,0,(sockaddr*)&peer,&len);
3.2给客户端发送消息
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
src_addr:要将数据发送给谁
addrlen:struct sockaddr结构体的大小
//处理客户端发送的消息,并且将结果返回给客户端
buffer[n] = 0;
sendto(_socketfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
四.客户端通信
4.1创建socket套接字
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
}
4.2客户端bind问题
客户端不需要显示的bind,os会自动帮你绑定端口号!!!
试想一下,你的手机上有抖音和微信两个客户端小程序,如果抖音客户端bind了8080这个端口,微信也想要bind8080这个端口,那么这时候就会出现一个问题,一个端口号被两个进程竞争!!!结果就是,抖音和微信不可能同时启动。
所以解决方法就是:udp client首次发送数据的时候,OS会自己自动随机的给client进行bind
4.3直接通信即可
4.3.1构建目标主机的socket信息
struct 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());
4.3.2给服务端发送消息
//给服务端发送消息
std::cout << "Please Enter# ";
std::getline(std::cin, message);
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
4.3.3.接收服务端发送过来的消息
//接收服务端发送过来的消息
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
char buffer[1024];
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(n > 0)
{
buffer[n] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
五.效果展示
5.1使用127.0.0.1本地环回测试
5.2使用公网ip测试
六.代码演示
6.1UdpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
#include "InetAddr.hpp"
enum
{
SOCKET_ERROR = 1,//创建套接字失败
BIND_ERROR,//bind绑定端口失败
USAGE_ERROR//启动udp服务失败
};
int default_socketfd = -1;
class UdpServer
{
public:
UdpServer(uint16_t prot): _socketfd(default_socketfd),_prot(prot)
{}
~UdpServer()
{}
void Init()
{
//1.创建socket套接字
_socketfd = socket(AF_INET,SOCK_DGRAM,0);
if(_socketfd < 0)
{
LOG(FATAL, "SOCKET ERROR, %s, %d\n", strerror(errno), errno);
exit(SOCKET_ERROR);
}
LOG(INFO, "socket create success, sockfd: %d\n", _socketfd);
//2.bind 将socket套接字和端口号进行绑定
//填充sockaddr_in结构
struct sockaddr_in local;
local.sin_family = AF_INET;//表明是网络通信
local.sin_port = htons(_prot);//将主机序列转成网络序列
local.sin_addr.s_addr = inet_addr("0.0.0.0");//将字符串类型的点分十进制ip转成四字节ip,并转成网络序列
//local.sin_addr.s_addr = INADDR_ANY;
//bind绑定端口号
int n = bind(_socketfd,(sockaddr *)&local,sizeof(local));//需要将sockaddr_in强转成sockaddr
if(n < 0)
{
LOG(FATAL, "BIND ERROR, %s, %d\n", strerror(errno), errno);
exit(BIND_ERROR);
}
LOG(INFO, "socket bind success\n");
}
void Stat()
{
//3.直接开始通信
while(true)
{
//接收客户端发送来的消息
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_socketfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);
if(n > 0)
{
InetAddr addr(peer);
LOG(DEBUG, "get message from [%s:%d]: %s\n", addr.Ip().c_str(), addr.Port(), buffer);
//处理客户端发送的消息,并且将结果返回给客户端
buffer[n] = 0;
sendto(_socketfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
}
}
}
private:
uint16_t _prot;
int _socketfd;
};
6.2UdpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
}
// 2. client要不要bind?一定要,client也要有自己的IP和PORT。要不要显式[和server一样用bind函数]的bind?不能!不建议!!
// a. 如何bind呢?udp client首次发送数据的时候,OS会自己自动随机的给client进行bind --- 为什么?防止client port冲突。要bind,必然要和port关联!
// b. 什么时候bind呢?首次发送数据的时候
// 构建目标主机的socket信息
struct 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());
std::string message;
// 2. 直接通信即可
while(true)
{
//给服务端发送消息
std::cout << "Please Enter# ";
std::getline(std::cin, message);
std::cout<<message<<std::endl;
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
//接收服务端发送过来的消息
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
char buffer[1024];
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(n > 0)
{
buffer[n] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
6.3InetAddr.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class InetAddr
{
private:
void GetAddress(std::string *ip, uint16_t *port)
{
*port = ntohs(_addr.sin_port);
*ip = inet_ntoa(_addr.sin_addr);
}
public:
InetAddr(const struct sockaddr_in &addr) : _addr(addr)
{
GetAddress(&_ip, &_port);
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
6.4LockGuard.hpp
#include <iostream>
#include <pthread.h>
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
{
pthread_mutex_lock(_mutex); // 构造加锁
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);// 析构释放锁
}
private:
pthread_mutex_t *_mutex;
};
6.5Log.hpp
#pragma once
#include <cstdio>
#include <iostream>
#include <string>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>
#include <fstream>
#include "LockGuard.hpp"
bool IsSave = false;//是否向文件中写入
const std::string logname = "log.txt";//日志信息写入的文件路径
// 日志是有等级的
enum Level
{
DEBUG = 0,
INFO,
WARNING,
ERROR,
FATAL
};
// 将日志的登记由整形转换为字符串
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";
}
}
// 获取时间
std::string GetTimeString()
{
time_t curr_time = time(nullptr);
struct tm *format_time = localtime(&curr_time);
if (format_time == nullptr)
return "None"; // 没有获取成功
char time_buffer[1024];
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
format_time->tm_year + 1900, // 这里的year是减去1900之后的值,需要加上1900
format_time->tm_mon + 1, // 这里的mon是介于0-11之间的,需要加上1
format_time->tm_mday,
format_time->tm_hour,
format_time->tm_min,
format_time->tm_sec);
return time_buffer;
}
//将日志信息写入到文件中
void SaveFile(const std::string &filename, const std::string &message)
{
std::ofstream out(filename, std::ios::app);
if (!out.is_open())
{
return;
}
out << message;
out.close();
}
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//定义锁 支持多线程
// 日志是有格式的
// 日志等级 时间 代码所在的文件名/行数 日志的内容
void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...)
{
std::string levelstr = LevelToString(level);
std::string timestr = GetTimeString();
pid_t selfid = getpid();
char buffer[1024];
va_list arg;//定义一个void* 指针
va_start(arg, format);//初始化指针,将指针指向可变参数列表开始的位置
vsnprintf(buffer, sizeof(buffer), format, arg);//将可变参数列表写入到buffer中
va_end(arg);//将指针置空
std::string message = "[" + timestr + "]" + "[" + levelstr + "]" +
"[" + std::to_string(selfid) + "]" +
"[" + filename + "]" + "[" + std::to_string(line) + "] " + buffer + "\n";
LockGuard lockguard(&lock);
if (!issave)
{
std::cout << message;//将日志信息打印到显示器中
}
else
{
SaveFile(logname, message);//将日志信息写入到文件
}
}
// C99新特性__VA_ARGS__
#define LOG(level, format, ...) do{ LogMessage(__FILE__, __LINE__,IsSave,level, format, ##__VA_ARGS__); }while(0)
#define EnableFile() do{ IsSave = true; }while(0)
#define EnableScreen() do{ IsSave = false; }while(0)
6.6main.cc
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " local_port\n" << std::endl;
}
// ./udpserver port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
usvr->Init();
usvr->Stat();
return 0;
}
6.7makefile
.PHONY:all
all:udpserver udpclient
udpclient:UdpClient.cc
g++ -o udpclient UdpClient.cc -std=c++14
udpserver:main.cc
g++ -o udpserver main.cc -std=c++14
.PHONY:clean
clean:
rm -f udpserver