目录
一,引言
今天我们要写的demo框架是基于CS架构的。也就是说要实现网络通信就必须要实现两端:1,客户端 2,服务端。这两端的任务如下:
服务端:
接收客户端发来的消息,并且打印显示出来,然后再向客户端反馈已收到消息。
客户端:
用户写下消息,并发送到服务端请求服务器处理。
二,服务端
1,server类
要实现网络通信必须要有的便是ip和端口号,所以我们的server类的成员当中一定要有的便是ip和端口号。并且我们的网络通信是依靠套接字完成的,所以在这个类中必不可少的还有套接字描述符。
所以类成员如下:
cpp
class UdpServer
{
private:
std::string ip_;//ip地址,ip地址一般都是点分十进制的所以用string
uint16_t port_;//端口号
int socketfd_;//套接字描述符
};
2,构造函数
server端的构造函数实现非常的简单,就是简单的让类内的成员被赋值。这一步可有可无,但是为了完整性还是写下。
代码:
cpp
UdpServer()
: port_(0), socketfd_(0)
{
}
3,初始化服务函数
初始化服务有以下两个步骤:1,创建套接字 2,绑定ip和port
创建套接字 :
使用socket函数创建:
cpp#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol);
domain:标识这个套接字的通信类型(本地/网络)
type:套接字提供的服务类型
protocol:协议
一般在实现UDP通信时填上0便可以。
返回值:socket函数的返回值是一个socket文件描述符。
代码:
cpp
socketfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
if(socketfd_<0)//创建失败就退出进程
{
perror("socket error\n");
exit(1);
}
绑定套接字:
使用bind函数绑定ip和port:
cpp#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符。
sockaddr:套接字地址结构体。
cppstruct sockaddr { sa_family_t sa_family; char sa_data[14]; }
addrlen:套接字结构体长度。
套接字地址结构体初始化:
cpp
sockaddr_in si;//定义套接字结构体地址
bzero(&si, sizeof si); // 清空
si.sin_family = AF_INET;//协议位ipv4
si.sin_port = htons(port);//端口号要转化为网络字节序列
si.sin_addr.s_addr = INADDR_ANY;//服务器端一般都要设置为可以接收任意ip地址发来的消息
开始绑定:
cpp
if(bind(socketfd_, (sockaddr *)&si, sizeof si)<0)//绑定失败就直接退出
{
perror("bind error\n");
exit(1);
}
初始化函数整体代码:
cpp
//定义一些变量来代表默认值
#define defaultip "0.0.0.0"
#define defaultport 8008
void Init(const std::string& ip = defaultip,int port = defaultport)
{
socketfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
if(socketfd_<0)//创建失败就退出进程
{
perror("socket error\n");
exit(1);
}
//创建成功,开始绑定
sockaddr_in si;//定义套接字结构体地址
bzero(&si, sizeof si); // 清空
si.sin_family = AF_INET;//协议位ipv4
si.sin_port = htons(port);//端口号要转化为网络字节序列
si.sin_addr.s_addr = INADDR_ANY;//服务器端一般都要设置为可以接收任意ip地址发来的消息
if(bind(socketfd_, (sockaddr *)&si, sizeof si)<0)//绑定失败就直接退出
{
perror("bind error\n");
exit(1);
}
}
4,启动函数
启动函数的作用如下:
1,接收客户端的请求。
2,将请求打印出来。
3,该函数是一个无限循环的函数。
使用recvfrom函数接收消息:
cpp
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
sockfd:服务器端的套接字描述符 。
buf:读取消息的缓冲区。
len:buf的长度 。
flags:读取消息的方式。
src_addr:自定义的地址结构体,用于存放用户端的ip地址和port。
addrlen:srd_addr的长度。
udp的服务是面向数据报的,也就是: SOCK_DGRAM。所以要使用recvfrom函数接收数据,而不能使用read。
使用sendto发消息:
cpp
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
sendto的参数和recvfrom的参数高度相似,这里就不一一介绍了。
整体代码如下:
cpp
void Run()
{
char inbuf[inbufSize];
sockaddr_in si;
socklen_t len;
while (true)
{
int r1 = recvfrom(socketfd_, inbuf, sizeof inbuf-1, 0, (sockaddr *)&si, &len);//读取消息
if(r1<0)//读取消息失败
{
perror("recvfrom error");
exit(10);
}
inbuf[r1] = 0;
std::string message = inbuf;
std::string tostring = "client say#" + message;
std::cout << message << std::endl;
const char *response = "收到消息,正在处理";
int r2 = sendto(socketfd_, response, sizeof response, 0, (sockaddr *)&si, sizeof si);
if(r2<0)
{
perror("server send message error");
continue;
}
}
}
写到这,服务端的代码就算结束了。接下来可以运行这个程序,然后使用netstat -naup指令查看当前服务器的挂起状态:
三,客户端
客户端的代码编写与服务端的代码编写其实差不多。客户端的代码做的事如下:
1,创建套接字。
2,自定义一个套接字结构体,并把该结构体的ip地址和端口号port初始化。
3,user写入消息。
4,将消息发送给服务端。
5,接收服务端的消息并打印出来。
代码如下:
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <cstdio>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define outbufSize 1024
class Client
{
public:
public:
Client()
: port_(0), socketfd_(0)
{
}
void Init(const std::string &ip, int port)
{
socketfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
if (socketfd_ < 0) // 创建失败就退出进程
{
perror("socket error\n");
exit(1);
}
port_ = port;
ip_ = ip;
// 不用bind
}
void Run()
{
std::string requestes;
sockaddr_in si;
socklen_t len;
si.sin_family = AF_INET;
si.sin_port = htons(port_);
si.sin_addr.s_addr = inet_addr(ip_.c_str());
char outbuf[outbufSize];
while (true)
{
std::cout << "请输入内容>> ";
std::getline(std::cin, requestes); // client输入内容
if (sendto(socketfd_, requestes.c_str(), sizeof requestes, 0, (sockaddr *)&si, sizeof si) < 0) // 发送消息
{
continue;
}
int r3 = recvfrom(socketfd_, outbuf, sizeof outbuf - 1, 0, (sockaddr *)&si, &len);
if(r3<0)
{
continue;
}
outbuf[r3] = 0;
std::cout << "server say# " << outbuf << std::endl;
memset(outbuf, 0, sizeof outbuf);
}
}
private:
std::string ip_; // ip地址
uint16_t port_; // 端口号
int socketfd_; // 套接字描述符
};
客户端的代码与服务端代码基本相似。
四,main函数调用
main函数主要起调用作用,在linux中我们的调用方式一般都是命令行的方式。如何使用命令行呢?在这里就不得不提到main函数的另一种形式了,如下:
cppint main(int argc,char* argv[])
argc:表示命令行中的字符串的数量。
argv:表示字符串参数。
如命令:
cppcd udp
argc = 2
argv[0] = cd argv[1]=udp
所以为了使用命令行的形式来调用服务端和客户端,我们的main函数也要写成这种形式。
cpp
#include "Client.hpp"
#include <memory>
void usage(const std::string &porc)命令行的输入必须符合格式:./可执行程序 ip地址 端口号
{
std::cout << porc << "ip:"
<< "port"
<< "[1024+]" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
std::string ip = argv[1];
int port = atoi(argv[2]);
std::unique_ptr<Client> Cp(new Client);
Cp->Init(ip,port);
Cp->Run();
return 0;
}
cpp
#include"Server.hpp"
#include<memory>
void usage(const std::string &porc)
{
std::cout << porc << "ip:"
<< "port"
<< "[1024+]" << std::endl;
}
int main(int argc,char*argv[])
{
if (argc != 2)//命令行的输入必须符合格式:./可执行程序 端口号
{
usage(argv[0]);
exit(1);
}
int port = atoi(argv[1]);
std::unique_ptr<UdpServer>Sp(new UdpServer);
Sp->Init(port);
Sp->Run();
return 0;
}
运行后结果如下:
调用时一定要保证客户端和服务端的端口号相同!!!