Socket 套接字的学习--UDP
上次我们大概介绍了一些关于网络的基础知识,这次我们利用编程来深入学习一下
一:套接字Socket
1.1什么是Socket

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
socket
我们可以用man 直接查这个socket,第一个参数是域或者协议家族,什么意思呢,就是以下这些
看它第二列目的,抵押给是本地通信,第二哥就是域间通信,第三个是IPV4,我们今天就用IPv4规定我们的IP地址。
第二个参数是代表套接字类型,也就是我们使用的TCP协议,还是UDP协议,我们今天用的UDP协议




这个是什么呢,##这个符号,叫做拼接符,什么啥意思呢,就是前面的参数拼接我后面的family,也就是现在它现在传的这个参数是sa_ ,那么拼接起来就是sa_family。
也就是sa_family 代表 用AF_INET 等类型初始化
但是我们刚才也说了sockaddr ,是作为最后的网络的统一的接口的,也就是说,我们在UDP通信的时候今天申请的是网络通信,所以用的是sockaddr_in,所以我们要用sockaddr_in这个结构体把网络信息存进去,最后强转成sockaddr。
我们看这个结构体,第一个就是 sin_family(刚才说的拼接)
第二个 sin_port ->端口号
第三个 sin_addr -> IP地址
所以我们要把我们这个服务端的协议族,端口号,IP地址存进去。
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());
当大家写的时候会发现,并不能直接将端口号直接赋值给sin_port,因为什么呢?因为我们上次说了网络中也是存在大小端的,就是有字节序的问题,所以我们要先转换成大端,也就是用htons函数
也不能直接把IP地址直接赋值给sin_addr,为什么呢?因为sin_addr它本身是一个结构体,我们还要在结构体里面找结构体成员进行赋值。
,因为我申请的IP用的是string 存放的,所以我想把这个村给s_addr(类型是32位4字节),就要转换
,用inet_addr()把char*转换成uint32_t 。
把网络信息存好,就可以进行绑定了。
//int n = bind(_sockfd, CONV(&local), sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";
1.5 接受网络消息--recvfrom
因为我们的UDP的传输报文方式是数据报形式的,所以用recvfrom
第一个参数依旧是老朋友,文教描述符,
第二个是一个void* buffer,意思就是存放信息的地方,
第三个是长度,
第四个默认位0就行,
第五个依旧就是统一接口sockaddr,代表发送端的网络信息(IP+Port)
第六个的socklen_t 代表sockaddr 的大小是多大的,用一个socklen_t的变量存起来然后取地址就可以。
ssize_t n = ::recvfrom ( _sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&len);
1.6 sendto 发送消息
依旧是因为UDP传输的数据报,选择sendto
第一个参数依旧是老朋友,文教描述符,
第二个是一个void* buffer,意思就是存放信息的地方,
第三个是长度,
第四个默认位0就行,
第五个依旧就是统一接口sockaddr,代表是发送端的网络信息
第六个的是sockaddr的大小,直接用sizeof就可以。
然后我们处理一下接受到的消息,之后再给客户端发送过去即可。
void Start(){LOG(LogLevel::INFO) << "start";while(true){//要接受客户端的信息//ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, *addrlen);char inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer) ;ssize_t n = ::recvfrom ( _sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&len);if(n>0){uint16_t clientport = ::ntohs(peer.sin_port);std::string clientip = ::inet_ntoa(peer.sin_addr);inbuffer[n] = 0;std::string clientinfo = clientip + ":" + std::to_string(clientport) + "#" + inbuffer;LOG(LogLevel::DEBUG) << clientinfo;//接受消息后 还有给客户端发送数据std::string echo_string = "echo#" ; echo_string += inbuffer;::sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,CONV(&peer),len);}else{LOG(LogLevel::FATAL) << "recvfrom: " << strerror(errno);}}
我们再说一个转换函数,这个要比inet_ntoa()函数转换更加安全
_ip = ::inet_ntop(AF_INET, &_local.sin_addr, ipbuffer, sizeof(ipbuffer));
inet_ntoa 本质呢是在内部有一个静态存储区,如果是多线程调用它,很容易进行覆盖,所以它并不安全,inet_ntop 这个函数是让你自己申请一个静态区,所以由程序员自己掌握,相对安全。
1.6 是否要绑定固定的 IP 和端口号
端口号有一定的定义范围
IP地址的定义范围
IPv4地址是一个32位的二进制数,通常被分为四段表示,每段8位,并以点分十进制(dotted-decimal notation)的形式表示,即每个字节转换为对应的十进制数字,各字节间用点号隔开。范围:0.0.0.0~255.255.255.255
特殊用途的IPv4地址范围包括:
私有地址:这些地址范围专门保留用于内部网络,不会在全球互联网路由中出现。10.0.0.0 ~ 10.255.255.255 172.16.0.0 ~ 172.31.255.255 192.168.0.0 ~ 192.168.255.255
回环地址:用于本机回环测试,通常使用127.0.0.1代表本地主机。
127.0.0.0 到 127.255.255.255
自动专用IP寻址:当无法从DHCP服务器获得配置时,系统可能会自动配置此范围内的地址。
169.254.0.0 到 169.254.255.255。
我们要不要再服务器中,把某个进程也就是它的端口号,和IP绑定在一起呢
答案是,不要,因为一个公司里的服务器,会有很多的IP地址,我们要给同一个端口发送消息,那么经过不同的IP,同一个端口号,都能给这个进程发送信息,但是如果你绑定了特定的IP,那么只有通过这个IP,才能给这个进程发送消息,所以我们不能绑定IP。
1.7 客户端
在我们学习完服务端之后,客户端就很简单了。
首先依旧是创建一个网络通信的文件,和上面一样。
然后区别来了:客户端用不用绑定网络信息呢??、
答案是:不用。为什么呢?难道客户端发送不需要对方知道自己的网络信息吗?并不是这样的,只是不用绑定,是因为系统回自动给客户端自由随机分配一个,为什呢?假设你的淘宝进程 你给绑定了80的端口号,而抖音也要这个端口号,那么不就起了冲突吗?而让系统自己分配,就可以避免这种冲突。只是不用手动绑定,而不是没有网络信息。
接着是一样的发送消息,接受消息。
这个的设计思路是,直接默认手输服务端的IP和端口,所以我们要用 argc 和 argv 两个参数。
if (argc != 3){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}ENABLE_CONSOLE_LOG();// ip + portstd::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = socket(AF_INET,SOCK_DGRAM,0);if(sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}struct sockaddr_in tmp;memset(&tmp, 0, sizeof(tmp));tmp.sin_family = AF_INET;tmp.sin_port=::htons(serverport);tmp.sin_addr.s_addr = inet_addr(serverip.c_str());while(true){std::cout<<"输入你要说的话"<<std::endl;std::string tstring;std::getline(std::cin,tstring);int n = ::sendto(sockfd,tstring.c_str(),tstring.size(),0,CONV(&tmp),sizeof(tmp));if(n<0){LOG(LogLevel::FATAL)<<"Client send to false";}char inbuffer[1024];struct sockaddr_in server;socklen_t len = sizeof(server);int m = ::recvfrom(sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&server),&len);if(m>0){inbuffer[m] = 0;std::cout<<inbuffer<<std::endl;}}
1.8 实现云平台和云平台直接的通信
那么你直接开两个终端,一个运行客户端,一个运行服务端,默认服务端 的端口号8080或者8888都可以。然后直接通信就可以了
1.9 云平台和Windows 的通信
首先你要在你的云平台的管理器那里,申请UDP通信,要保证你的UDP端口是可以用的。接下来给大家分享一个Windows端作为用户端的代码,云平台依旧使用我们刚才的服务端的代码就可以
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
std::string serverip = ""; //自己云平台的IP
uint16_t serverport = 8080;//默认一个端口号就可以
int main()
{WSADATA wsd;WSAStartup(MAKEWORD(2, 2), &wsd);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());SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == SOCKET_ERROR){std::cout << "socker error" << std::endl;return 1;}std::string message;char buffer[1024];while (true){std::cout << "Please Enter@ ";std::getline(std::cin, message);if (message.empty()) continue;sendto(sockfd, message.c_str(), (int)message.size(), 0,(struct sockaddr*)&server, sizeof(server));struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}closesocket(sockfd);WSACleanup();return 0;
}
网络的代码一样,只是在Windows下,某些文件需要提前配置和打开关闭。
2.0奉上全部代码
UDPClient.cc
#include<iostream>
#include "Log.hpp"
#include "UdpServer.hpp"
#include <string>
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace LogModule;
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}ENABLE_CONSOLE_LOG();// ip + portstd::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = socket(AF_INET,SOCK_DGRAM,0);if(sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}struct sockaddr_in tmp;memset(&tmp, 0, sizeof(tmp));tmp.sin_family = AF_INET;tmp.sin_port=::htons(serverport);tmp.sin_addr.s_addr = inet_addr(serverip.c_str());while(true){std::cout<<"输入你要说的话"<<std::endl;std::string tstring;std::getline(std::cin,tstring);int n = ::sendto(sockfd,tstring.c_str(),tstring.size(),0,CONV(&tmp),sizeof(tmp));if(n<0){LOG(LogLevel::FATAL)<<"Client send to false";}char inbuffer[1024];struct sockaddr_in server;socklen_t len = sizeof(server);int m = ::recvfrom(sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&server),&len);if(m>0){inbuffer[m] = 0;std::cout<<inbuffer<<std::endl;}}return 0;
}
UDPServer.hpp
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const static std::string fault_ip = "127.0.0.1";
const static uint16_t fault_port = 8080;
class UDPServer
{
public:UDPServer(std::string ip = fault_ip, uint16_t port = fault_port): _ip(ip), _port(port), _isrunning(false), _sockfd(-1){}void InitServer(){_sockfd = socket(AF_INET, SOCK_DGRAM, 0); //这是创建好了一个网络上的文件if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket create false";Die(USAGE_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;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());//要绑定网络上的信息 你要知道是谁传的 ip + portint n = bind(_sockfd, CONV(&local), sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void Start(){LOG(LogLevel::INFO) << "start";_isrunning = true;while(true){//要接受客户端的信息// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,// struct sockaddr *src_addr, *addrlen);char inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer) ;ssize_t n = ::recvfrom ( _sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&len);if(n>0){uint16_t clientport = ::ntohs(peer.sin_port);std::string clientip = ::inet_ntoa(peer.sin_addr);inbuffer[n] = 0;std::string clientinfo = clientip + ":" + std::to_string(clientport) + "#" + inbuffer;LOG(LogLevel::DEBUG) << clientinfo;//接受消息后 还有给客户端发送数据std::string echo_string = "echo#" ; echo_string += inbuffer;::sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,CONV(&peer),len);}else{LOG(LogLevel::FATAL) << "recvfrom: " << strerror(errno);}}}~UDPServer(){}private:int _sockfd; // 建立的网络文件描述符std::string _ip;uint16_t _port;bool _isrunning;
};#endif
UDPServer.cc
#include<iostream>
#include "Log.hpp"
#include "UdpServer.hpp"
#include <string>
#include "Common.hpp"
using namespace LogModule;
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}ENABLE_CONSOLE_LOG();std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);std::unique_ptr<UDPServer> svr_uptr = std::make_unique<UDPServer>(ip,port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}
Common.hpp
#pragma once#include <iostream>#define Die(code) \do \{ \exit(code); \} while (0)#define CONV(v) (struct sockaddr *)(v)enum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR
};