目錄
一.創建socket套接字(服務器端)
二.bind將prot與端口號進行綁定(服務器端)
2.1填充sockaddr_in結構
2.2bind綁定端口
三.直接通信(服務器端)
3.1接收客戶端發送的消息
3.2給客戶端發送消息
四.客戶端通信
4.1創建socket套接字
4.2客戶端bind問題
4.3直接通信即可
4.3.1構建目標主機的socket信息
4.3.2給服務端發送消息
4.3.3.接收服務端發送過來的消息
五.效果展示
5.1使用127.0.0.1本地環回測試
5.2使用公網ip測試
六.代碼演示
6.1UdpServer.hpp
6.2UdpClient.cc
6.3InetAddr.hpp
6.4LockGuard.hpp
6.5Log.hpp
6.6main.cc
6.7makefile
一.創建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強轉成sockaddrif(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. 創建socketint 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之后的值,需要加上1900format_time->tm_mon + 1, // 這里的mon是介于0-11之間的,需要加上1format_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.ccg++ -o udpclient UdpClient.cc -std=c++14
udpserver:main.ccg++ -o udpserver main.cc -std=c++14
.PHONY:clean
clean:rm -f udpserver