【Linux】:socket编程——UDP
朋友们、伙计们,我们又见面了,本期来给大家带来socket编程相关的知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
目录
1. 端口号
1.1 pid和port
2. IP协议和UDP协议
3. 网络字节序
4. socket编程
4.1 socket 常见API
4.2 sockaddr 结构
4.3 代码实现简易UDP通信
1. 端口号
端口号(port)是传输层协议的内容:
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用。
两个主机进行网络通信时实际上是两个进程进行通信;
所有的网络通信行为:本质都是进程间通信,但是不在一台机器上;
所以在进行通信时,对于双方而言:
① 先保证数据能到达自己这台机器(IP);
② 找到指定的进程(port)。
所以IP标识主机唯一,端口号(port)标识进程唯一。
IP和port用来标识互联网中唯一一个进程。 (ip和port合起来就叫做一组套接字)
1.1 pid和port
我们之前学习过的进程pid不就可以用来标识进程的唯一性嘛,为什么要有port呢?
① 网络进程和port可以进行绑定关联;
② 进程管理和网络进行解耦;
③ port专门用来进行网络通信。
一个进程可以和一个或者多个端口号进行关联,但是一个端口号只能和一个进程进行关联。
2. IP协议和UDP协议
这里只对IP和UDP进行简单认识;
IP协议:
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP协议:
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
这里的可靠和不可靠不是标识的谁好谁坏,而是两个协议是不同的;
3. 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端,就需要先将数据转成大端; 否则就忽略,直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
- h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
4. socket编程
4.1 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); // 发送信息 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // 接收信息 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // 开始监听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);
网络编程时,socket有很多类别:
- ① unix socket:域间 socket,用同一台机器上文件路径,类似于命名管道,用于本主机内部进行通信;
- ② 网络socket:ip + port进行网络通信;
- ③ 原始socket:编写一些网络工具。
既然有这么多类型,所以根据不同的场景,应该给每一种场景都设计一套编程接口,但是这样做太麻烦了,所以设计者就用一套接口来设计,其中,struct sockaddr 就是一个通用的地址类型。
4.2 sockaddr 结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同;
当我们使用sockaddr时,会根据前两个字节来判别进行哪种通信!
sockaddr 结构
sockaddr_in 结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址
in_addr结构
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;
4.3 代码实现简易UDP通信
Comm.hpp
#pragma once enum{ Usage_Err = 1, Socket_Err, Bind_Err };
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> 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;
nocopy.hpp
// 禁止拷贝 #pragma once #include <iostream> class nocopy { public: nocopy(){} nocopy(const nocopy &) = delete; const nocopy& operator = (const nocopy &) = delete; ~nocopy(){} };
Main.cc
#include "UdpServer.hpp" #include "Comm.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[]) { if(argc != 3) { Usage(argv[0]); return Usage_Err; } // 获取填入的ip和port const std::string ip = argv[1]; const uint16_t port = std::stoi(argv[2]); // 构造UdpServer对象 std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip,port); usvr->Init(); usvr->Start(); return 0; }
UdpServer.hpp
#pragma once #include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <cerrno> #include <cstring> #include <strings.h> #include <netinet/in.h> #include <arpa/inet.h> #include "nocopy.hpp" #include "Log.hpp" #include "Comm.hpp" const static uint16_t defaultport = 8080; const static int defaultfd = -1; const static int defaultsize = 1024; class UdpServer : public nocopy { public: UdpServer(const std::string &ip, uint16_t port = defaultport) :_ip(ip), _port(port), _sockfd(defaultfd) {} void Init() { // 1. 创建套接字 _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd < 0) { lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno)); exit(Socket_Err); } lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd); // 2. 绑定--指定网络信息 // 设置结构体 struct sockaddr_in local; bzero(&local, sizeof(local)); // memset local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 绑定至内核 int n = ::bind(_sockfd,(struct sockaddr*)&local,sizeof(local)); if(n != 0) { lg.LogMessage(Fatal, "bind errr, %d : %s\n", errno, strerror(errno)); exit(Bind_Err); } } void Start() { // 服务器启动了就不能停止了 char buffer[defaultsize]; for(;;) { struct sockaddr_in peer; socklen_t len = sizeof(peer); // 接收消息 ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len); if(n > 0) { // 接收成功 buffer[n] = 0; std::cout << "client say# " << buffer << std::endl; // 发送消息 sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len); } } } ~UdpServer(){} private: std::string _ip; uint16_t _port; int _sockfd; };
UdpClient.cc
#include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <cerrno> #include <cstring> #include <string> #include "Log.hpp" #include "Comm.hpp" const static int defaultsize = 1024; void Usage(std::string process) { std::cout << "Usage: " << process << " server_ip server_port" << std::endl; } int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); return Usage_Err; } // 获取服务器IP和端口 std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); // 创建套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno)); exit(Socket_Err); } lg.LogMessage(Info, "socket success, sockfd: %d\n", sockfd); // 填写server端结构体 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()); while (true) { // 输入 std::string inbuffer; std::cout << "Please Enter# "; std::getline(std::cin, inbuffer); // 发消息 ssize_t n = sendto(sockfd, inbuffer.c_str(), sizeof(inbuffer), 0, (struct sockaddr *)&server, sizeof(server)); if (n > 0) // 发送成功 { char buffer[defaultsize]; // 收消息 struct sockaddr_in temp; socklen_t len = sizeof(temp); ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len); if (m > 0) { buffer[m] = 0; std::cout << "server echo# " << buffer << std::endl; } else break; } else break; } close(sockfd); return 0; }
Makefile
.PHONY:all all:udp_server udp_client udp_server:Main.cc g++ -o $@ $^ -std=c++14 udp_client:UdpClient.cc g++ -o $@ $^ -std=c++14 .PHONY:clean clean: rm -f udp_server udp_client
需要注意的是:在实现client时我们填写了结构体是不需要显示的bind,client会在首次发送数据的时候会自动进行bind,因为server端的端口号是众所周知的,不可改变的,client端不一定只有一个,有可能有很多个client同时链接server端,所以client 需要bind,但是不需要显示bind,让本地OS自动随机bind,选择随机端口号。
我们可以在此基础上更新出另外两个版本:
① 通过udp通信实现一个远程执行命令的版本:https://gitee.com/yue-sir-bit/linux/tree/master/2.udp_server_excute
② 通过udp于线程池实现一个远程聊天室版本
https://gitee.com/yue-sir-bit/linux/tree/master/3.udp_server_chat