TCP/IP网络编程
环境介绍
操作系统:Ubuntu20.04
实现语言:C++
编译器:gcc version 9.3.0
编译工具:cmake version 3.16.3
编辑器:VS Code
远程工具:VS Code
socket编程简介
网络编程领域需要一定的操作系统、计算机网络的相关知识。至少要对于基本的概念有所理解,这样才有助于网络编程的实现。就个人目前的理解来说,对socket编程的相关操作类似于对串口进行的操作。无非是,USB是设备间的物理接口,socket是设备之间的“网络接口”。两者的操作方式有着诸多相似之处,可以对比理解。
学习网络编程可以参考教材《TCP/IP网络编程》(韩)尹圣雨。
TCP
TCP是面向连接的,使用面向字节流的套接字。这种传输方式提供可靠交付,有流量控制,拥塞控制,提供全双工通信,每一条TCP连接只能是点对点的,即一对一的传输方式。使用socket创建TCP连接的函数调用过程如下所示:
(1)服务端函数调用过程
(2)客户端函数调用过程
服务端
-
创建socket
在linux操作系统下,我们使用int socket (int __domain, int __type, int __protocol)
函数创建socket,创建过程如下所示:
int serv_sock;
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
//PF_INET IPV4互联网协议族,Protocol Family的缩写
//SOCK_STREAM 面向字节流的套接字
//IPPROTO_UDP 为第三个参数,如果写0,则自动匹配,因为满足PF_INET, SOCK_STREAM的只有TCP
//该函数如果创建成功则返回一个文件描述符,否则返回-1表示创建失败
-
分配套接字地址
我们需要给创建好的socket分配地址,主要包含了IP地址:端口
信息。这一过程对于服务器端来说是必须的,因为服务器需要监听此端口上的连接请求。在linux系统中,我们可以使用函数int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
给socket绑定地址信息。使用方法如下所示:
int portNum = 5200;
struct sockaddr_in serv_adr;
memset(&serv_adr, 0, sizeof(serv_adr)); //把服务端地址初始化填充为0
serv_adr.sin_family = AF_INET; //初始化服务端协议地址族类型
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); //服务端IP地址,4字节所以使用long转换
serv_adr.sin_port = htons(portNum);
//AF_INET表示IPV4网络协议使用的地址族(Address Family的缩写)
//sin_addr 使用了in_addr结构体,其中有一个成员变量为In_addr_t类型,成员名s_addr,存储32位IPV4地址
//htons和htonl为字节序列转换函数,h表示host主机字节序,to表示转换,n表示网络字节序,s表示short,l表示long,即把short类型数据的长度进行转换
//INADDR_ANY表示一个服务器上的所有网卡地址即0.0.0.0表示主机本身
bind(serv_sock, (sockaddr *)&serv_adr, sizeof(serv_adr)) //将创建的socket绑定到对应的地址上
//将socket绑定到对应的地址(包含了端口信息)上
//参数含义为socket 地址信息指针 地址信息长度
-
监听连接请求
在调用bind函数以后,接下来就要通过listen函数进入等待连接请求状态,如果服务端不进入监听状态,那么客户端调用connect函数发出连接请求则会出错。linux操作系统中listen函数的声明为int listen(int __fd, int __n)
,使用方法如下所示:
listen(serv_sock,5);
//其中第一个参数为服务器的套接字,第二个参数为连接请求队列的长度,表示了最多使5个连接请求进入队列
//连接请求队列的长度于服务器端的特性有关
-
允许连接
调用listen函数后,如果有新的连接请求,那么服务器应当按照顺序受理。服务器端可以使用int accept(int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len)
函数受理客户端的连接请求,此过程中,函数会自动创建新的套接字用于连接到发起请求的客户端。使用方法如下所示:
clnt_adr_size = sizeof(clnt_adr);
clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_size);
//参数依次为服务器socket,客户端地址指针,客户端地址长度
//如果函数调用成功,将自动产生用于数据I/O的套接字,并返回其文件描述符,即clnt_sock
//返回的套接字是自动创建的并且会自动与发起请求的客户端建立连接
-
数据交换
服务器端受理了客户端的请求以后,就可以使用read()
或者write()
函数进行数据交换了。两个函数的原型如下所示:
//参数依次为文件描述符(socket),存放数据的buffer指针,buffer的长度
//函数的返回值为读取的长度,如果读取失败返回-1,如果达到结尾返回0
ssize_t read (int __fd, void *__buf, size_t __nbytes);
//参数依次为目标文件描述符(用来与客户端进行数据交换的socket),存放数据的buffer指针,发送的数据长度
//函数的返回值为写入的长度,如果写入失败则返回-1
ssize_t write (int __fd, const void *__buf, size_t __n);
使用方法如下所示:
write(clntSocket,buffer,sizeof(buffer));
-
断开连接
使用close()函数关闭套接字。
客户端
客户端与服务端相比要简单的多,因为客户端的流程比较简单:创建套接字、请求连接、数据交换、关闭套接字。这里仅简单介绍以下请求连接的函数connect()
。
函数声明如下所示:
//connect用于请求到目标地址的连接
//函数如果执行成功,返回0;如果执行出错,返回-1;
int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
UDP
UDP是没有连接的,使用面向数据报的套接字。这种传输方式强调快速传输而非顺序,传输的数据可能丢失也可能损毁,初始的数据有数据边界,限制每次传输的数据大小,因此是“不可靠的、不按顺序的、以数据的高速传输为目的的套接字”。由于UDP协议是没有连接的,所以客户端和服务端没有连接的过程,只有创建套接字和数据交换的过程。使用socket创建UDP通信的函数调用过程如下所示:
基本过程
-
创建socket
socket在linux下就是一个文件,所以声明类型为int型,使用int socket (int __domain, int __type, int __protocol)
函数进行创建。使用方法如下:
int serv_sock;
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
//PF_INET IPV4互联网协议族,Protocol Family的缩写
//SOCK_DGRAM 面向数据报的套接字
//IPPROTO_TCP 为第三个参数,如果写0,则自动匹配,因为满足PF_INET, SOCK_DGRAM的只有UDP
//该函数如果创建成功则返回一个文件描述符,否则返回-1表示创建失败
-
分配套接字地址
创建好的socket需要和相应的地址信息绑定,使用的函数为int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
。对于服务端来说这一过程是必须的,因为需要知道接收哪个IP地址:端口
的消息;对于客户端来说不是必须的,但是可以通过这种方式来指定socket使用的端口号,否则会默认分配。那么,显然就需要有一个类型来描述相应的信息,即sockaddr_in
类型,最好能够详细了解下这个结构体的成员变量及其含义方便理解和使用,这里不详细介绍,只简单说明使用方法。
int portNum = 5200;
struct sockaddr_in serv_adr;
memset(&serv_adr, 0, sizeof(serv_adr)); //把服务端地址初始化填充为0
serv_adr.sin_family = AF_INET; //初始化服务端协议地址族类型
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); //服务端IP地址,4字节所以使用long转换
serv_adr.sin_port = htons(portNum);
//AF_INET表示IPV4网络协议使用的地址族
//sin_addr 使用了in_addr结构体,其中有一个成员变量为In_addr_t类型,成员名s_addr,存储32位IPV4地址
//htons和htonl为字节序列转换函数,h表示host主机字节序,to表示转换,n表示网络字节序,s表示short,l表示long,即把short类型数据的长度进行转换
//INADDR_ANY表示一个服务器上的所有网卡地址即0.0.0.0表示主机本身
bind(serv_sock, (sockaddr *)&serv_adr, sizeof(serv_adr)) //将创建的socket绑定到对应的地址上
//将socket绑定到对应的地址(包含了端口信息)上
//参数含义为socket 地址信息指针 地址信息长度
-
发送与接收
发送和接收的函数比较简单,分别为sendto
和recvfrom
,其函数原型分别如下:
//参数依次表示: socket 发送数据的buffer地址 发送数据的长度 标志位 数据目标的地址信息指针 地址长度
//返回值为发送的数据的字节数,发送失败则会返回-1
ssize_t sendto (int __fd, const void *__buf, size_t __n, int __flags,
__CONST_SOCKADDR_ARG __addr, socklen_t __addr_len);
//参数依次表示: socket 接收数据的buffer地址 buffer的最大地址 标志位 数据来源的地址信息指针 地址长度的指针
//返回值为接收数据的字节数,接收失败则会返回-1
ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n, int __flags,
__SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len);
具体的使用方法如下所示:
clnt_adr_sz = sizeof(clnt_adr);
//接收函数会自动检测到来源的地址信息,从而更新地址的成员变量
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (sockaddr *)&clnt_adr, &clnt_adr_sz);
//将收到的信息回传,所以直接发到源地址上即可,这样便实现了 回声-UDP服务器/客户端
sendto(serv_sock, message, str_len, 0, (sockaddr *)&clnt_adr, clnt_adr_sz);
-
关闭socket
socket最后需要使用close()函数进行关闭,以释放相关资源。
阻塞与非阻塞
默认情况下,recvfrom函数是阻塞的,也就是说如果接收不到消息会一直等待。但是,这样的机制不能很好的适用特定的场景,所以可以设置socket为非阻塞模式,这样如果收不到数据就不会阻塞,可以继续其他的处理,设置方法如下所示,创建socket之后使用以下的函数设置为非阻塞模式:
void setnonblocking(int sockfd) {
int flag = fcntl(sockfd, F_GETFL, 0);
if (flag < 0) {
std::cout<<"fcntl F_GETFL fail!"<<std::endl;
return;
}
if (fcntl(sockfd, F_SETFL, flag | O_NONBLOCK) < 0) {
std::cout<<"fcntl F_GETFL fail!"<<std::endl;
}
}
Demo
这里分别使用基于TCP和UDP协议的回声服务器作为演示,此代码来自于教材《TCP/IP网络编程》(韩)尹圣雨,并做了适当的改动。
基于TCP的回声服务器
基于TCP的回声服务器的主要效果为,客户端向服务器发送连接请求并发送数据,服务端收到请求后,将从客户端接收到的数据原封不动的返回给客户端,以此实现类似的效果。这里使用TCP协议来实现。
服务端代码
运行服务端的时候需要指定端口,因此程序运行时需要指定端口号。代码如下所示:
#include<bits/stdc++.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
using namespace std;
#define BUFFERSIZE 50
void errorHandling(string msg){
cout<<"ERROR: "<<msg<<endl;
exit(1);
}
int main(int argc,char* argv[]){
int servSocket,clntSocket; //用于监听的套接字和用于数据I/O的套接字
int queueSize = 5; //监听队列的大小
struct sockaddr_in servAddr,clntAddr;
socklen_t clntAddrSize;
char buffer[BUFFERSIZE] = {0};
//检查参数
if(argc!=2)
errorHandling("argument error!");
//初始化服务器端套接字
servSocket = socket(PF_INET,SOCK_STREAM,0);
if(servSocket == -1)
errorHandling("socket() error!");
//初始化服务器端地址
memset(&servAddr,0,sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
//绑定地址和套接字
if(bind(servSocket,(struct sockaddr*)&servAddr,sizeof(servAddr))==-1)
errorHandling("bind() error!");
//监听
if(listen(servSocket,queueSize)==-1)
errorHandling("listen() error!");
//循环接收客户端请求,并为每个客户端创建一个新的套接字
//服务完5个客户端后退出
for(int i=0;i<queueSize;i++){
//受理客户端请求
clntAddrSize = sizeof(clntAddr);
clntSocket = accept(servSocket,(struct sockaddr*)&clntAddr,&clntAddrSize);
if(clntSocket == -1){
errorHandling("accept() error");
}
//接收客户端消息并回复,直到接收不到数据表示当前客户端已经断开连接
int recvLen;
while((recvLen = read(clntSocket,buffer,BUFFERSIZE))!=0){
cout<<"receive msg from client : "<<string(buffer,recvLen)<<endl;
write(clntSocket,buffer,recvLen);
}
//完成当前客户端的服务后关闭服务端数据I/O的套接字
close(clntSocket);
}
close(servSocket);
return 0;
}
将以上源代码文件编译成名为echo_tcp_server
的可执行文件,并使用以下命令启动服务端,指定端口为9600:
./echo_tcp_server 9600
客户端代码
TCP客户端代码比较简单,主要的工作便是完成连接请求,然后进行数据传输,代码如下所示:
#include<bits/stdc++.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
using namespace std;
#define BUFFERSIZE 50
void errorHandling(string msg){
cout<<"ERROR: "<<msg<<endl;
exit(1);
}
int main(int argc,char* argv[]){
int sock; //用于数据I/O的套接字
struct sockaddr_in servAddr;
char buffer[BUFFERSIZE] = {0};
//检查参数
if(argc!=3)
errorHandling("argument error!");
//初始化客户端套接字
sock = socket(PF_INET,SOCK_STREAM,0);
if(sock == -1)
errorHandling("socket() error!");
//初始化服务器端地址
memset(&servAddr,0,sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]);
servAddr.sin_port = htons(atoi(argv[2]));
//连接服务器
if(connect(sock,(struct sockaddr*)&servAddr,sizeof(servAddr))==-1)
errorHandling("connect() error!");
//发送消息并接收回复
string msg;
while(true){
cout<<"input msg to send (q or Q to quit) : ";
getline(cin,msg);
if(msg == "q" || msg == "Q")
break;
write(sock,msg.c_str(),msg.size());
memset(buffer,0,BUFFERSIZE);
if(read(sock,buffer,BUFFERSIZE) == -1){
errorHandling("read() error!");
} else {
cout<<"receive msg from server : "<<string(buffer)<<endl;
}
}
close(sock);
return 0;
}
将以上源代码文件编译成名为echo_tcp_client
的可执行文件,并使用以下命令启动服务端,指定服务器地址为本机(回环地址),指定端口为9600:
./echo_tcp_client 127.0.0.1 9600
效果展示
以下为编译运行后的实际效果演示:
基于UDP的回声服务器
基于UDP的回声服务器的主要效果为,客户端向服务器发送一条数据,服务端将收到的数据原封不动的返回给客户端,以此实现一个类似回声的效果。这里使用UDP协议来实现。
服务端代码
运行服务端的时候需要指定端口,所以运行时程序需要端口作为参数,并从此端口接收客户端数据。代码如下所示:
#include<bits/stdc++.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
using namespace std;
#define BUF_SIZE 50
void error_handling(char *messages);
int main(int argc, char const *argv[]){
cout << "This is server!" << endl;
int serv_sock; //服务端socket
char message[BUF_SIZE]; //消息buffer
int str_len; //长度
socklen_t clnt_adr_sz; //地址长度
struct sockaddr_in serv_adr, clnt_adr; //地址结构体
if (argc != 2){ //校验main函数的参数
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
//PF_INET IPV4互联网协议族
//SOCK_DGRAM 面向消息的套接字
//IPPROTO_UDP 为第三个参数,如果写0,则自动匹配,因为满足PF_INET, SOCK_DGRAM的只有UDP
serv_sock = socket(PF_INET, SOCK_DGRAM, 0); //创建server端的socket
if (serv_sock == -1){ //创建失败的提示
error_handling("UDP socket creation error");
}
//AF_INET表示IPV4网络协议使用的地址族
//sin_addr 使用了in_addr结构体,其中有一个成员变量为In_addr_t类型,成员名s_addr,存储32位IPV4地址
//htons和htonl为字节序列转换函数,h表示host主机字节序,to表示转换,n表示网络字节序,s表示short,l表示long,即把short类型数据的长度进行转换
//INADDR_ANY表示一个服务器上的所有网卡地址即0.0.0.0表示主机本身
memset(&serv_adr, 0, sizeof(serv_adr)); //把服务端地址初始化填充为0
serv_adr.sin_family = AF_INET; //初始化服务端协议地址族类型
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); //服务端IP地址,4字节所以使用long转换
serv_adr.sin_port = htons(atoi(argv[1])); //服务端的端口号,2字节所以用short转换
//将socket绑定到对应的地址(包含了端口信息)上
//参数socket 地址信息 地址长度
if (bind(serv_sock, (sockaddr *)&serv_adr, sizeof(serv_adr)) == -1){
error_handling("bind() error");
}
while (true){
clnt_adr_sz = sizeof(clnt_adr);
//接收函数会自动检测到来源的地址信息,从而更新内部的参数
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (sockaddr *)&clnt_adr, &clnt_adr_sz);
//将收到的信息回传,所以直接发到源地址上即可,这样便实现了 回声-UDP服务器/客户端
sendto(serv_sock, message, str_len, 0, (sockaddr *)&clnt_adr, clnt_adr_sz);
}
//关闭socket
close(serv_sock);
cout << "udp_server ended!" << endl;
return 0;
}
void error_handling(char *messages){
fputs(messages, stderr);
fputc('\n', stderr);
exit(1);
}
将以上源代码文件编译成名为echo_server
的可执行文件,并使用以下命令启动服务端,指定端口为9600:
./echo_server 9600
客户端代码
客户端和服务端类似,因为UDP没有连接,所以没有创建连接的过程,只需要直接发送即可。客户端向服务器指定的端口发送数据,并接收来服务器返回的数据,代码如下:
#include<bits/stdc++.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
using namespace std;
#define BUF_SIZE 50
void error_handling(char *messages);
int main(int argc, char const *argv[]){
cout << "This is client!" << endl;
int sock; //socket
char message[BUF_SIZE]; //数据buffer
int str_len; //字符数据的长度
socklen_t adr_sz; //地址长度
struct sockaddr_in serv_adr, from_adr; //服务器地址和来源地址
if (argc != 3){ //main函数参数校验
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
//创建socket
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1){
error_handling("socket() error");
}
//inet_addr(const char* string)函数传入ip地址字符串,返回一个32为大端序列整数值,失败返回INADDR_NONE
//inet_aton函数可以把传入ip地址字符串转为in_addr结构体类型,内部实现调用了inet_addr,所以直接赋值给sin_addr变量即可
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]); //将字符串转化为相应的类型
// std::cout << "serv_adr.sin_addr.s_addr: " << serv_adr.sin_addr.s_addr << endl;
serv_adr.sin_port = htons(atoi(argv[2])); //端口
while(true){
fputs("Insert message(q to quit): ", stdout);//从命令行读取需要发送的消息,q表示退出
fgets(message, sizeof(message), stdin);
if(!strcmp(message,"q\n")||!strcmp(message,"Q\n"))
break;
//UDP是没有连接的,所以需要先给服务器发送消息,服务器才知道回复给谁
sendto(sock, message, strlen(message),0,(sockaddr *)&serv_adr, sizeof(serv_adr));
//从服务器接收消息
adr_sz = sizeof(from_adr);
str_len = recvfrom(sock, message, BUF_SIZE, 0, (sockaddr *)&from_adr, &adr_sz);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
cout << "udp_client ended!" << endl;
return 0;
}
void error_handling(char *messages){
fputs(messages, stderr);
fputc('\n', stderr);
exit(1);
}
将以上源代码文件编译成名为echo_client
的可执行文件,并使用以下命令启动服务端,指定服务器ip地址(这里使用本机做测试,所以使用回环地址),指定端口为9600:
./ehco_client 127.0.0.1 9600
基于UDP的短消息转发服务器
根据回声服务器的示例代码,可以做一定的改造和封装,编写一个基于UDP的短消息转发服务器(具有公网IP地址),可以实现两个私有网络中客户端之间的消息转发。
实现方案
服务器端使用两个socket分别负责和两个客户端之间的通信,互斥访问缓冲区以实现上下行数据的交换。
服务端类的封装
头文件udp_server.h
:
#pragma once
#include<bits/stdc++.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<sys/socket.h>
#include<thread>
#include<mutex>
class UDP_SERVER{
private:
enum SZIE{BUFF_SIZE = 512};
//两个socket
int serv_sock_1;
int serv_sock_2;
//两个端口号
int port_1;
int port_2;
//两个服务端地址
sockaddr_in serv_adr_1;
sockaddr_in serv_adr_2;
//两个客户端地址及其长度
sockaddr_in clnt_adr_1;
socklen_t clnt_adr_1_sz;
sockaddr_in clnt_adr_2;
socklen_t clnt_adr_2_sz;
//上下行buffer的互斥锁
std::mutex upstream_buffer_mutex;
std::mutex downstream_buffer_mutex;
//处理上下行数据的线程指针
std::thread* process_upstream_thread;
std::thread* process_downstream_thread;
//上下行buffer及其长度
char *upstream_buff;
char *downstream_buff;
int up_buff_sz;
int down_buff_sz;
public:
UDP_SERVER();
~UDP_SERVER();
//初始化函数
bool init(int portA = 50210, int portB = 50220);
//线程函数
void process_upstream();
void process_downstream();
void run();
//设置socket为非阻塞
static void setnonblocking(int sockfd);
};
类的实现udp_server.cpp
:
#include "udp_server.h"
UDP_SERVER::UDP_SERVER(){
//do nothing
serv_sock_1 = -1;
serv_sock_2 = -1;
port_1 = -1;
port_2 = -1;
up_buff_sz = 0;
down_buff_sz = 0;
upstream_buff = nullptr;
downstream_buff = nullptr;
process_upstream_thread = nullptr;
process_downstream_thread = nullptr;
}
UDP_SERVER::~UDP_SERVER(){
//关闭socket
if (serv_sock_1 != -1){
close(serv_sock_1);
}
if (serv_sock_2 != -1){
close(serv_sock_2);
}
//销毁上下行buffer
if (upstream_buff)
delete upstream_buff;
if (downstream_buff)
delete downstream_buff;
//销毁线程指针
if (process_upstream_thread)
delete process_upstream_thread;
if (process_downstream_thread)
delete process_downstream_thread;
}
bool UDP_SERVER::init(int portA, int portB){
port_1 = portA;
port_2 = portB;
//创建socket
serv_sock_1 = socket(PF_INET, SOCK_DGRAM, 0);
serv_sock_2 = socket(PF_INET, SOCK_DGRAM, 0);
if (serv_sock_1 == -1 || serv_sock_2 == -1){
std::cout << "UDP socket creation error!" << std::endl;
return false;
}
//设置为非阻塞
UDP_SERVER::setnonblocking(serv_sock_1);
UDP_SERVER::setnonblocking(serv_sock_2);
//填写两个socket的地址信息
memset(&serv_adr_1, 0, sizeof(serv_adr_1)); //把服务端地址初始化填充为0
serv_adr_1.sin_family = AF_INET; //初始化服务端协议地址族类型
serv_adr_1.sin_addr.s_addr = htonl(INADDR_ANY); //服务端IP地址,4字节所以使用long转换
serv_adr_1.sin_port = htons(port_1);
memset(&serv_adr_2, 0, sizeof(serv_adr_2)); //把服务端地址初始化填充为0
serv_adr_2.sin_family = AF_INET; //初始化服务端协议地址族类型
serv_adr_2.sin_addr.s_addr = htonl(INADDR_ANY); //服务端IP地址,4字节所以使用long转换
serv_adr_2.sin_port = htons(port_2);
//bind()过程
if (bind(serv_sock_1, (sockaddr *)&serv_adr_1, sizeof(serv_adr_1)) == -1){
std::cout << "bind() error!" << std::endl;
return false;
}
if (bind(serv_sock_2, (sockaddr *)&serv_adr_2, sizeof(serv_adr_2)) == -1){
std::cout << "bind() error!" << std::endl;
return false;
}
//为上下行buffer申请空间
upstream_buff = new char[BUFF_SIZE];
downstream_buff = new char[BUFF_SIZE];
if (!upstream_buff || !downstream_buff){
return false;
}
//创建线程
process_upstream_thread = new std::thread(&UDP_SERVER::process_upstream, this);
process_downstream_thread = new std::thread(&UDP_SERVER::process_downstream, this);
if (!process_downstream_thread || !process_downstream_thread){
return false;
}
return true;
}
void UDP_SERVER::process_upstream(){
char buffer[BUFF_SIZE];
int len;
while (true){
//读取消息
clnt_adr_1_sz = sizeof(clnt_adr_1);
len = recvfrom(serv_sock_1, buffer, BUFF_SIZE, 0, (sockaddr *)&clnt_adr_1, &clnt_adr_1_sz);
if (len > 0){
//填充上行buffer
upstream_buffer_mutex.lock();
memcpy(upstream_buff, buffer, len);
up_buff_sz = len;
upstream_buffer_mutex.unlock();
//输出客户端的信息
std::cout << std::endl;
std::cout << "clnt_adr_1 Addr: " << inet_ntoa(clnt_adr_1.sin_addr) << std::endl;
std::cout << "Port: " << ntohs(clnt_adr_1.sin_port) << std::endl;
}
//获取下行消息,并清空
len = 0;
memset(buffer, 0, BUFF_SIZE);
downstream_buffer_mutex.lock();
if (down_buff_sz > 0){
memcpy(buffer, downstream_buff, down_buff_sz);
len = down_buff_sz;
memset(downstream_buff, 0, BUFF_SIZE);
down_buff_sz = 0;
}
downstream_buffer_mutex.unlock();
//如果下行消息不空则转发给自己负责的客户端
if (len > 0){
sendto(serv_sock_1, buffer, len, 0, (sockaddr *)&clnt_adr_1, clnt_adr_1_sz);
std::cout << "Send message " << buffer << " from 2 to 1" << std::endl;
}
usleep(10);
}
}
void UDP_SERVER::process_downstream(){
char buffer[BUFF_SIZE];
int len;
while (true){
//读取消息
clnt_adr_2_sz = sizeof(clnt_adr_2);
len = recvfrom(serv_sock_2, buffer, BUFF_SIZE, 0, (sockaddr *)&clnt_adr_2, &clnt_adr_2_sz);
if (len > 0){
//填充下行buffer
downstream_buffer_mutex.lock();
memcpy(downstream_buff, buffer, len);
down_buff_sz = len;
downstream_buffer_mutex.unlock();
//输出客户端的信息
std::cout << std::endl;
std::cout << "clnt_adr_2 Addr: " << inet_ntoa(clnt_adr_2.sin_addr) << std::endl;
std::cout << "Port: " << ntohs(clnt_adr_2.sin_port) << std::endl;
}
//获取上行消息,并清空
len = 0;
memset(buffer, 0, BUFF_SIZE);
upstream_buffer_mutex.lock();
if (up_buff_sz > 0){
memcpy(buffer, upstream_buff, up_buff_sz);
len = up_buff_sz;
memset(upstream_buff, 0, BUFF_SIZE);
up_buff_sz = 0;
}
upstream_buffer_mutex.unlock();
//如果上行消息不空则转发
if (len > 0){
sendto(serv_sock_2, buffer, len, 0, (sockaddr *)&clnt_adr_2, clnt_adr_2_sz);
std::cout << "Send message " << buffer << " from 1 to 2" << std::endl;
}
usleep(10);
}
}
void UDP_SERVER::run(){
process_upstream_thread->join();
process_downstream_thread->join();
}
void UDP_SERVER::setnonblocking(int sockfd) {
int flag = fcntl(sockfd, F_GETFL, 0);
if (flag < 0) {
std::cout<<"fcntl F_GETFL fail!"<<std::endl;
return;
}
if (fcntl(sockfd, F_SETFL, flag | O_NONBLOCK) < 0) {
std::cout<<"fcntl F_GETFL fail!"<<std::endl;
}
}
服务端类的使用
服务端的使用比较简单,创建对象后使用init()函数进行初始化,如果初始化成功,则调用run函数。使用示例如下:
#include"udp_server.h"
#include<iostream>
int main(){
UDP_SERVER udp_server;
if(udp_server.init())
udp_server.run();
std::cout << "UDP_SERVER ENDED!" << std::endl;
return 0;
}
客户端类的封装
客户端的方案是使用两个线程分别对外进行收发操作,类本身通过外部接口与使用者进行数据交换,这样隐藏了内部发送和接收的细节。使用时,只需要初始化相关资源,然后通过接口给客户端类提供数据用于发送,或者从客户端获取接收的内容,因为udp是没有连接的,所以必须同时保证两次都有数据收发服务器才能正确的获取到两个客户端的地址信息。
头文件udp_client.h
:
#pragma once
#include<bits/stdc++.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<sys/socket.h>
using namespace std;
class UDP_CLIENT{
private:
int sock;
string ip;
int port;
socklen_t adr_sz;
sockaddr_in serv_adr;
sockaddr_in from_adr;
string recv_data;
string send_data;
std::mutex sock_mutx;
std::mutex recv_data_mutx;
std::mutex send_data_mutx;
std::thread *recv_thread;
std::thread *send_thread;
bool runing;
public:
UDP_CLIENT();
~UDP_CLIENT();
bool init(string ipAddr, int portNum);
void recvData();
void sendData();
string getRecvData();
void setSendData(string data);
void waiting();
bool isRuning();
static void setnonblocking(int sockfd);
};
类的实现udp_client.cpp
:
#include"udp_client.h"
UDP_CLIENT::UDP_CLIENT(){
//donothing
sock = -1;
runing = false;
}
UDP_CLIENT::~UDP_CLIENT(){
if (sock != -1){
close(sock);
}
if (recv_thread)
delete recv_thread;
if (send_thread)
delete send_thread;
}
bool UDP_CLIENT::init(string ipAddr, int portNum){
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1){
puts("socket() error");
return false;
}
setnonblocking(sock);
ip = ipAddr;
port = portNum;
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(ip.c_str());
serv_adr.sin_port = htons(port); //服务器的端口号
recv_thread = new thread(&UDP_CLIENT::recvData, this);
send_thread = new thread(&UDP_CLIENT::sendData, this);
if (!recv_thread || !send_thread){
return false;
}
runing = true;
return true;
}
void UDP_CLIENT::recvData(){
const int BUF_SIZE = 512;
std::cout << "recv Data threading starting......" << endl;
while (1){
char message[BUF_SIZE]={0};
int str_len;
adr_sz = sizeof(from_adr);
//接收数据
sock_mutx.lock();
str_len = recvfrom(sock, message, BUF_SIZE, 0, (sockaddr *)&from_adr, &adr_sz);
sock_mutx.unlock();
if (str_len > 0){
//如果收到数据,则填充接收缓冲区
recv_data_mutx.lock();
recv_data.clear();
for (int i = 0; i < str_len; i++){
recv_data.push_back(message[i]);
}
std::cout << "Recv Data: " << recv_data << std::endl;
recv_data_mutx.unlock();
}
usleep(10);
}
}
void UDP_CLIENT::sendData(){
int ret = -1;
std::cout << "send Data threading starting......" << endl;
while(1){
string data;
send_data_mutx.lock();
data = send_data;
send_data_mutx.unlock();
//如果要发送的数据为空,则跳过
if(data.empty()){
usleep(20);
continue;
}
//发送数据
sock_mutx.lock();
ret = sendto(sock, data.c_str(), data.size(), 0, (sockaddr *)&serv_adr, sizeof(serv_adr));
sock_mutx.unlock();
if (ret != -1){
std::cout << "Send Data: " << data << std::endl;
//如果发送成功则清空发送缓冲池区数据
send_data_mutx.lock();
send_data.clear();
send_data_mutx.unlock();
}
usleep(10);
}
}
string UDP_CLIENT::getRecvData(){
string ret;
//如果有读取的数据,那么获取数据返回并清空
recv_data_mutx.lock();
if(recv_data.size()){
ret = recv_data;
recv_data.clear();
}
recv_data_mutx.unlock();
return ret;
}
void UDP_CLIENT::setSendData(string data){
send_data_mutx.lock();
send_data = data;
send_data_mutx.unlock();
}
void UDP_CLIENT::waiting(){
recv_thread->join();
send_thread->join();
}
bool UDP_CLIENT::isRuning(){
return runing;
}
void UDP_CLIENT::setnonblocking(int sockfd){
int flag = fcntl(sockfd, F_GETFL, 0);
if (flag < 0) {
puts("fcntl F_GETFL fail");
return;
}
if (fcntl(sockfd, F_SETFL, flag | O_NONBLOCK) < 0) {
puts("fcntl F_SETFL fail");
}
}
客户端的使用
客户端的使用比较简单,如果初始化成功,那么就可以给客户端提供发送的数据,或者获取客户端获取的内容。由于主线程要等待读取和写入线程,所以需要使用join来实现等待,waiting()方法就是在判断线程已经成功启动后调用线程的join()让主线程等待。客户端的使用方法如下,两个客户端可以使用相同的结构:
#include"udp_client.h"
int main(){
UDP_CLIENT udp_client;
string ip = "127.0.0.1";
int port = 50210;
std::cout << "This is Client 1 !" << endl;
if(udp_client.init(ip, port)){
while (true){
string words = "Client 1 : hello server ," + to_string(time(0));
udp_client.setSendData(words);
// std::cout << "Send : " << words << std::endl;
string recvData = udp_client.getRecvData();
if(recvData.size())
cout << "Now: " << time(0) <<", RecvData: "<< recvData << endl;
sleep(5);
}
}
if(udp_client.isRuning())
udp_client.waiting();
return 0;
}
构建项目
使用cmake工具构建项目并进行测试,这里将项目名称UDP_SERVER
目录结构
目录结构如下:
├── CMakeLists.txt
├── include
│ ├── udp_client.h
│ └── udp_server.h
└── src
├── clientone.cpp
├── clienttwo.cpp
├── server.cpp
├── udp_client.cpp
└── udp_server.cpp
CMakeLists文件如下:
cmake_minimum_required(VERSION 3.0.0)
project(USV_SERVER)
set(CMAKE_CXX_FLAGS "${CAMKE_CXX_FLAGS} -std=c++11 -pthread")
include_directories(./include)
add_executable(udp_server ./src/server.cpp ./src/udp_server.cpp)
add_executable(client_one ./src/clientone.cpp ./src/udp_client.cpp)
add_executable(client_two ./src/clienttwo.cpp ./src/udp_client.cpp)
测试方法
分别启动服务器和两个客户端,观察两个客户端数据的收发情况以及服务端数据转发情况,以上代码中都添加了相应的输出提示,可以通过对比两个客户端数据的时间戳来计算链路的延迟。
效果测试
这里在一台机器上的测试效果如下所示:
服务器部分输出数据如下:
clnt_adr_1 Addr: 127.0.0.1
Port: 60868
Send message Client 1 : hello server ,1646225376 from 1 to 2
clnt_adr_2 Addr: 127.0.0.1
Port: 57397
Send message Client 2 : hello server ,1646225378 from 2 to 1
clnt_adr_1 Addr: 127.0.0.1
Port: Send message Client 1 : hello server ,1646225381 from 1 to 260868
clnt_adr_2 Addr: 127.0.0.1
Port: 57397
Send message Client 2 : hello server ,1646225383 from 2 to 1
客户端1部分输出数据如下:
This is Client 1 !
recv Data threading starting......
send Data threading starting......
Send Data: Client 1 : hello server ,1646225376
Recv Data: Client 2 : hello server ,1646225378
Now: 1646225381, RecvData: Client 2 : hello server ,1646225378
Send Data: Client 1 : hello server ,1646225381
Recv Data: Client 2 : hello server ,1646225383
Now: 1646225386, RecvData: Client 2 : hello server ,1646225383
Send Data: Client 1 : hello server ,1646225386
Recv Data: Client 2 : hello server ,1646225388
Now: 1646225391, RecvData: Client 2 : hello server ,1646225388
客户端2部分输出数据如下:
This is Client 2 !
recv Data threading starting......
send Data threading starting......
Send Data: Client 2 : hello server ,1646225378
Recv Data: Client 1 : hello server ,1646225381
Now: 1646225383, RecvData: Client 1 : hello server ,1646225381
Send Data: Client 2 : hello server ,1646225383
Recv Data: Client 1 : hello server ,1646225386
Now: 1646225388, RecvData: Client 1 : hello server ,1646225386
Send Data: Client 2 : hello server ,1646225388
Recv Data: Client 1 : hello server ,1646225391
Now: 1646225393, RecvData: Client 1 : hello server ,1646225391
总结
因为临时计划使用udp来传输短指令,所以使用两天时间简单学习并实现udp转发服务器的客户端和服务端类的封装,简单记录一下。
Comments | NOTHING