五、UDP网络编程
1.对于服务器使用智能指针维护生命周期;2.创建UDP套接字;3.绑定端口号,包括设置服务器端口号和IP地址,端口号一般是2字节使用uint16_t,而IP地址用户习惯使用点分十进制格式所以传入的是string类型,同时要保证网络字节序列;4.执行run;
UDP通信的socket是全双工的,允许被同时读写的;
5.1使用接口
5.1.1创建套接字
int socket(int domain, int type, int protocol);
//第一个参数为域/协议家族AF_UNIX, AF_LOCAL表示域间套接字,AF_INET ,AF_INET6表示网络套接字,AF_PACKET表示原始套接字,除了AF开头也可以PF开头;
//第二个参数表示socket的类型SOCK_STREAM表示流式套接字,SOCK_DGRAM表示数据包套接字;
//第三个参数如果只有一个协议则使用0;
//返回值是socket文件的文件描述符,打开的是网卡设备;
5.1.2进行绑定
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
struct sockaddr {sa_family_t sa_family;char sa_data[14];
}
//第一个参数是创建好的套接字文件描述符;
//第二个参数是输入型参数,一个特定的结构需要使用其他结构进行强转;
//第三个参数特定类型的长度
//返回值成功为0失败为-1,错误码被设置;
#include <strings.h>
void bzero(void *s, size_t n);
#include <string.h>
void *memset(void *s, int c, size_t n);
//使用上述函数进行清空
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family
struct sockaddr_in
{__SOCKADDR_COMMON (sin_);in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. *//* Pad to size of `struct sockaddr'. */unsigned char sin_zero[sizeof (struct sockaddr) -__SOCKADDR_COMMON_SIZE -sizeof (in_port_t) -sizeof (struct in_addr)];
};
sockaddr_in结构中,sin表示socket inet
sin_zero表示填充字段
sin_port表示端口号字段
sin_family表示使用的协议家族如AF_INET之类的
sin_addr表示的是IP地址结构
//local.sin_addr.s_addr = INADDR_ANY;//任意地址绑定
//字符串转整数接口
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);//将字符串转32位并且是网络序列的;
//整数转字符串
char *inet_ntoa(struct in_addr in);//将整数转为字符串并且将网络字节序转为主机字节序
5.1.3数据报的读取
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
//第四个参数默认使用0以阻塞方式
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
5.1.4数据报的发送
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
5.2补充知识
1.查看网络状态
netstat -nlupa
#查看网络状态,n表示能显示成数字的就都显示成数字,l表示listen u表示udp,p表示进程,a表示所有;
2.IP问题
云服务器禁止直接绑定公网IP;配置较高的机器IP地址不唯一;只是绑定一个IP地址就会导致另一个IP地址的发往另一个IP地址的数据无法被接收;一般使用的是0,这样凡是发送给本主机的数据都要根据端口号向上交付,而不是先确定IP;即IP地址使用0表示的是任意地址绑定;
3.端口号问题
云服务器端口号默认0-1023是不允许绑定的,是系统内定的端口号,一般由固定的应用层协议使用如,http-80,https-443,MySQL-3306,普通用户直接使用的一般是1024以后的端口号,端口号的范围是0-65535,可以在这个范围内使用;
服务器需要绑定显式端口号但是客户端不需要显式绑定,操作系统会随机绑定;因为一个端口号只能被一个进程绑定,所以重复绑定同一个端口号会出错;防止出现这种冲突,就不允许用户显示绑定而是交给了操作系统,只要保证端口号的唯一性就可以,这样就不会导致不同软件的客户端出现绑定端口号冲突;一般服务器需要显示绑定端口号是因为一般是客户端先发送的请求,所以需要知道服务端的IP地址和端口号,且端口号不能发生变化必须是固定的,这样才能让客户端知道要发送的另一端进程是谁;
系统会在客户端首次发送数据的时候进行端口号的绑定;客户端可以获取多个服务器的信息所以获取信息时要包含服务端的信息;
4.本地环回地址
127.0.0.1是本地环回地址,通常来进行cs的测试;
5.3echo服务器实现
服务端
#include <sys/types.h>
#include <sys/socket.h>
#include "log.hpp"
#include <string>
#include <arpa/inet.h>
#include <cstring>
#include <cstdlib>
#include <netinet/in.h>
#include <functional>using func_t = std::function<std::string(const std::string &)>;
extern Log lg;const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";
const int size = 1024;enum
{SOCKETERR = 1,BINDERR,
};class udpserver
{public:udpserver(const uint16_t &port = defaultport, const std::string &ip = defaultip) : ip_(ip), port_(port), isrunning_(false) {}~udpserver(){if (sockfd_ > 0){close(sockfd_);}}public:void init(){// 1.创建socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d", sockfd_);exit(SOCKETERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);// 2.bindstruct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_);// local.sin_addr.s_addr = inet_addr(ip_.c_str());local.sin_addr.s_addr = INADDR_ANY;int n = bind(sockfd_, (sockaddr *)&local, sizeof(local));if (n < 0){lg(Fatal, "bind error, errno: %d, strerror: %s", errno, strerror(errno));exit(BINDERR);}lg(Info, "bind success, errno: %d, strerror: %s", errno, strerror(errno));}void run(func_t func){isrunning_ = true;char buff[size];sockaddr_in client;socklen_t sz = sizeof(client);while (isrunning_){ssize_t n = recvfrom(sockfd_, buff, sizeof(buff) - 1, 0, (sockaddr *)&client, &sz);if (n < 0){lg(Warning, "recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));continue;}buff[n] = 0;// 处理数据std::string info = buff;std::string echo_string = func(info);sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (sockaddr *)&client, sz);}}private:int sockfd_; // 网络文件描述符std::string ip_; // IP地址,1.将字符串风格转换为uint_t类型,2.转换成网络字节序列uint16_t port_; // 端口号转换成网络字节序列bool isrunning_;
};--
void Usage(string proc)
{cout << "\n\rUsage: " << proc << " port[1024+]" << endl;
}string handler(const string &str)
{cout << str << endl;string ret = "server get a message: ";ret += str;return ret;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}uint16_t port = stoi(argv[1]);unique_ptr<udpserver> svr(new udpserver(port, "127.0.0.1"));svr->init();svr->run(handler);return 0;
}
客户端
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <cstdlib>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
extern Log lg;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);return 0;}std::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;exit(1);}sockaddr_in server;bzero(&server, 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;char buff[1024];while (true){std::cout << "client say@ ";std::getline(std::cin, message);sendto(sockfd, message.c_str(), message.size(), 0, (sockaddr *)&server, sizeof(server));sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t n = recvfrom(sockfd, buff, sizeof(buff) - 1, 0, (sockaddr *)&temp, &len);if (n > 0){buff[n] = 0;std::cout << buff << std::endl;}}close(sockfd);return 0;
}
5.4服务器实现执行指令
#include <stdio.h>
FILE *popen(const char *command, const char *type);
//是一个封装起来的管道和子进程执行命令的接口,会自动建立管道,让子进程执行命令并将子进程的执行结果通过管道返回给父进程;
//第一个参数是子进程要执行的命令,第二个参数是对打开文件的方式,读写;
//返回值是用来进行获取执行结果的;
int pclose(FILE *stream);
char *fgets(char *s, int size, FILE *stream);
//读取一行,当读到EOF/空时返回nullptr;
bool safecheck(const string &cmd)
{int issafe = false;vector<string> keyword;keyword.push_back("rm");keyword.push_back("mv");keyword.push_back("kill");for (auto &e : keyword){auto pos = cmd.find(e);if (pos != string::npos){return issafe;}}issafe = true;return issafe;
}string execute(const string &cmd)
{if (!safecheck(cmd)){return "unsafe";}FILE *fp = popen(cmd.c_str(), "r");if (fp == nullptr){perror("popen error: ");return "error";}char buff[4096];string ret;while (true){char *res = fgets(buff, sizeof(buff), fp);if (res == nullptr){break;}ret += buff;}cout << "get a cmd: " << cmd << endl;pclose(fp);return ret;
}
Xshell的原理就是,远端执行了一个22服务,即ssh,客户端发送指令,服务端进行执行后返回结果;
5.5Windows和Linux通信
#include<iostream>
#include<WinSock2.h>//网络套接字接口#pragma comment(lib,"ws2_32.lib")//链接静态库using namespace std;int main()
{cout << "hello client" << endl;WSADATA wsd;//创建变量int n = WSAStartup(MAKEWORD(2, 2), &wsd);//初始化//通信过程WSACleanup();//释放变量return 0;
}
#define _WINSOCK_DEPRECATED_NO_WARNINGS 1
#include<iostream>
#include<WinSock2.h>//网络套接字接口
#include<string>#pragma comment(lib,"ws2_32.lib")//链接静态库using namespace std;
const uint16_t serverport = 8080;
const string serverip = "60.205.138.126";
int main()
{cout << "hello client" << endl;WSADATA wsd;int n = WSAStartup(MAKEWORD(2, 2), &wsd);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socket error" << endl;}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;char buff[1024];while (true){std::cout << "client say@ ";std::getline(std::cin, message);sendto(sockfd, message.c_str(), message.size(), 0, (sockaddr*)&server, sizeof(server));sockaddr_in temp;int len = sizeof(temp);int n = recvfrom(sockfd, buff, sizeof(buff) - 1, 0, (sockaddr*)&temp, &len);if (n > 0){buff[n] = 0;std::cout << buff << std::endl;}}closesocket(sockfd);WSACleanup();return 0;
}
5.6制作一个聊天室,获取用户IP
1.recvfrom不仅可以获取收到的信息,还可以获取发送方的sockaddr_in信息,用来知道发送方的信息;2.将处理数据和产生数据用回调函数的方式来进行软件分层,也可以类内创建一个函数处理;3.客户端使用getline会阻塞住,并不能获取其他客户端发来的消息,所以需要使用多路复用将接口设置为非阻塞才能解决此问题;4.可以将客户端多线程化解决;
vs2019下客户端
#define _WINSOCK_DEPRECATED_NO_WARNINGS 1
#include <iostream>
#include <WinSock2.h> //网络套接字接口
#include <string>
#include <thread>#pragma comment(lib, "ws2_32.lib") // 链接静态库
using namespace std;
const uint16_t serverport = 8080;
const string serverip = "60.205.138.126";void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}void recvr(int sockfd)
{char buff[1024];while (true){sockaddr_in temp;int len = sizeof(temp);int n = recvfrom(sockfd, buff, sizeof(buff) - 1, 0, (sockaddr*)&temp, &len);if (n > 0){buff[n] = 0;std::cout << buff << std::endl;}}
}void sendr(int sockfd, sockaddr_in server)
{std::string message;while (true){std::cout << "vs2019 say@ ";std::getline(std::cin, message);sendto(sockfd, message.c_str(), message.size(), 0, (sockaddr*)&server, sizeof(server));}
}int main(int argc, char* argv[])
{WSADATA wsd;//创建变量int n = WSAStartup(MAKEWORD(2, 2), &wsd);//初始化int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;exit(1);}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());thread r(recvr, sockfd);thread s(sendr, sockfd, server);r.join();s.join();closesocket(sockfd);WSACleanup();return 0;
}
g++下客户端
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <cstdlib>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include "log.hpp"
extern Log lg;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}struct threadinfo
{threadinfo(int sockfd, const sockaddr_in &server) : sockfd_(sockfd){server_.sin_family = server.sin_family;server_.sin_port = server.sin_port;server_.sin_addr.s_addr = server.sin_addr.s_addr;}int sockfd_;sockaddr_in server_;
};void *recv(void *args)
{threadinfo *td = static_cast<threadinfo *>(args);char buff[1024];while (true){sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t n = recvfrom(td->sockfd_, buff, sizeof(buff) - 1, 0, (sockaddr *)&temp, &len);if (n > 0){buff[n] = 0;std::cout << buff << std::endl;}}
}void *send(void *args)
{threadinfo *td = static_cast<threadinfo *>(args);std::string message;while (true){std::cout << "tencent say@ ";std::getline(std::cin, message);sendto(td->sockfd_, message.c_str(), message.size(), 0, (sockaddr *)&td->server_, sizeof(td->server_));}
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);return 0;}std::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;exit(1);}sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());pthread_t rtid, stid;threadinfo *td = new threadinfo(sockfd, server);pthread_create(&rtid, nullptr, recv, (void *)td);pthread_create(&stid, nullptr, send, (void *)td);pthread_join(rtid, nullptr);pthread_join(stid, nullptr);close(sockfd);return 0;
}
效果展示
5.7使用终端模拟图形化界面将输入和输出分开
/dev/pts
#存放的是打开的终端文件,可以直接输出重定向到终端文件里面
echo "hello">/dev/pts/0
const std::string terminal = "/dev/pts/2";void openTerminal()
{int fd = open(terminal.c_str(), O_WRONLY);dup2(fd, 2);
}
./client 60.205.138.126 8080 2>/dev/pts/2
#也可以实现重定向
Linux中两个机器之间传输文件
scp client dyh@82.156.23.226:/home/dyh
#将本机文件发送到另一端Linux机器
scp dyh@60.205.138.126:/home/dyh/11/client .
#从其他Linux机器获取文件