一.简介
文章将讲解简单的echo服务器(阻塞)搭建的过程,介绍各函数的用法和定义。
二.头文件结构体定义和源文件函数实现
1.TcpStream.h
结构体定义
cpp
#pragma once
#include<string>
#include<cstdint>
using namespace std;
class TcpStream
{
public:
explicit TcpStream(int sock=-1); //显式构造,防止隐式转换
~TcpStream(); //析构函数,自动关闭socket
bool send_message(const string msg); //发送一条完整消息(长度头+消息体)
string recv_message(); //接受一条完整消息,失败返回空串
bool is_valid() const { return sock_!=1; } //判断对象是否有效
//手动关闭
void close();
private:
int sock_; //socket文件描述符
bool send_all(int sock,const char* buf,size_t len); //发送全部消息
bool receive_all(int sock,char* buf,size_t len); //接收全部消息
};
is_valid()函数用于判断创建的socket或者接收连接后的socket是否有效。
函数实现
cpp
#include"TcpStream.h"
#include<sys/socket.h>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <iostream>
#include<arpa/inet.h>
using namespace std;
//发送消息静态辅助函数
bool TcpStream::send_all(int sock,const char *buf,size_t len)
{
size_t sent=0; //记录已发送的数据字节数
while(sent<len)
{
ssize_t n=send(sock,buf+sent,len-sent,0);
if(n==-1) //发送错误
{
return false;
}
sent +=n;
}
return true;
}
//接收消息静态辅助函数
bool TcpStream::receive_all(int sock,char *buf,size_t len)
{
size_t received=0; //记录已接收的数据字节数
while(received<len)
{
ssize_t n=recv(sock,buf+received,len-received,0);
if(n<=0)
{
if(n==-1) cout<<"receive error"<<endl;
if(n==0) cout<<"对端关闭连接"<<endl;
return false;
}
received +=n;
}
return true;
}
//构造函数
TcpStream::TcpStream(int sock):sock_(sock){};
//析构函数
TcpStream::~TcpStream(){
close();
}
//发送消息
bool TcpStream::send_message(const string msg)
{
if(!is_valid()) return false;
uint32_t net_len=htonl(static_cast<uint32_t>(msg.size()));
if(!send_all(sock_,reinterpret_cast<char*>(&net_len),4)){
return false;
}
if(!send_all(sock_,msg.c_str(),msg.size())){
return false;
}
return true;
}
//接收消息
string TcpStream::recv_message()
{
if(!is_valid()) return "";
uint32_t net_len;
if(!receive_all(sock_,reinterpret_cast<char*>(&net_len),4)){
return "";
}
uint32_t len=ntohl(net_len);
if(len>1024*1024) return "";
string buf(len,'\0');
if(!receive_all(sock_,&buf[0],len)){
return "";
}
return buf;
}
void TcpStream::close()
{
if(sock_!=-1)
{::close(sock_);
sock_=-1;}
}
- bool send_all(int sock,const* buf,size_t len)和bool receive_all(int sock,const* buf,size_t len)函数是辅助发送和接收函数,函数中会分别用到send和recv函数。
- 函数bool send_message(con string msg)用于发送一条完整的数据,包含长度头和消息体,其中长度头用于标志消息体的长度和防止粘包。客户端发送数据和服务端发送反馈到客户端用到此函数。此函数实现会调用send_all和receive_all函数。
返回值:成功返回true失败返回false。 - 函数string recv_message()用于接收一条完整的数据,同样包含长度头和消息体。客户端接收服务端反馈和服务端接收客户端的消息用到此函数。
返回值:成功返回接收到的消息,失败返回空string字符串。 - close()函数用于关闭socket接口。
2.TcpServer.h
cpp
#pragma once
#include "TcpStream.h"
#include<cstdint>
class TcpServer{
public:
//构造函数:监听指定窗口
TcpServer(uint16_t port,int backlog=10);
//析构函数:关闭监听
~TcpServer();
//检查服务器是否成功启动
bool is_valid() const {return listen_fd !=-1;}
//接受一个新连接,返回新的socket文件描述符,失败返回-1
int accept_();
// 手动关闭监听 socket
void close();
private:
int listen_fd;
};
函数实现
cpp
#include "TcpServer.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <iostream>
using namespace std;
//构造函数
TcpServer::TcpServer(uint16_t port,int backlog):listen_fd(-1) //backlog指定队列最大长度
{
//创建socket
listen_fd=::socket(AF_INET,SOCK_STREAM,0);
if(listen_fd==-1)
{
cerr<<"socket error"<<strerror(errno)<<endl;
return;
}
//设置端口重用
int opt=1;
if(::setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))==-1)
{
cerr<<"setsockopt() error"<<strerror(errno)<<endl;
close();
return;
}
//绑定地址
sockaddr_in addr{};
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=htonl(INADDR_ANY); //监听所有端口
addr.sin_port=htons(port);
if(::bind(listen_fd,(sockaddr*)&addr,sizeof(addr))==-1)
{
cerr<<"bind() error"<<strerror(errno)<<endl;
close();
return;
}
//开始监听
if(::listen(listen_fd,backlog)==-1)
{
cerr<<"listen() error"<<strerror(errno)<<endl;
close();
return;
}
cout<<"Server listening on port:"<<port<<endl;
}
TcpServer::~TcpServer() {
close();
}
int TcpServer::accept_()
{
if(!is_valid()) return -1;
sockaddr_in client_addr;
socklen_t len=sizeof(client_addr);
int client_fd=::accept(listen_fd,(sockaddr*)&client_addr,&len);
if (client_fd == -1) {
std::cerr << "accept_() error: " << strerror(errno) << std::endl;
return -1;
}
//显示客户端ip和端口(用于调试)
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET,&client_addr.sin_addr,ip,sizeof(ip));
cout<<"来自"<<ip<<" : "<<ntohl(client_addr.sin_port)<<"的新连接"<<endl;
return client_fd;
}
void TcpServer::close() {
if (listen_fd != -1) {
::close(listen_fd);
listen_fd = -1;
}
}
- 初始化构造函数TcpServer时创建一个监听接口,然后设置端口重用(为了避免服务器关闭后立即重启时因
TIME_WAIT状态导致绑定失败),然后绑定ip地址和端口,开始监听。 - accept_函数负责接收请求连接的socket,然后创建新的socket即client_fd,用于后续数据的发送和接收,然后输出已连接的客户端的ip地址和端口号。
三.服务端和客户端的main函数
1.服务端
cpp
#include "TcpServer.h"
#include "TcpStream.h"
#include<iostream>
using namespace std;
int main()
{
uint16_t port;
cout<<"请输入要监听的端口(目前只有8888):";
cin>>port;
TcpServer server(port);
if(!server.is_valid())
{
cerr<<"服务端启动失败"<<endl;
return 1;
}
cout<<"Server listening on port:"<<port<<endl;
while(true)
{
int client_fd=server.accept_();
if(client_fd==-1) continue; //出错继续下一个循环
TcpStream client(client_fd);
if(!client.is_valid()) continue;
bool client_error =false;
while(!client_error)
{
string msg=client.recv_message();
if(msg.empty()){
client_error=true;
break;
}
string response="服务端接收到:"+msg;
cout<<response<<endl;
if(!client.send_message(response)) {
cout<<"发送反馈失败"<<endl;
client_error=true;
break;
}
}
}
return 0;
}
- 输入端口号后初始化TcpServer对象server,初始化时完成了监听接口listen_fd的创建,端口重用的设置,然后绑定ip和端口,最后开始监听。
- 监听到请求连接的客户端后进入while循环,创建新接口client_fd接收连接,然后用client_fd初始化TcpStream对象,用client_error标志客户端是否出错,进入第二个while循环,开始接收客户端发送的消息,由于是阻塞状态,所以一次只能单线程处理一个客户端。接收消息完毕(完整的消息)后向客户端发送反馈。
2.客户端
cpp
#include "TcpStream.h"
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
int main() {
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock==-1)
{
cerr<<"创建socket出错"<<endl;
return 1;
}
//配置ip和端口
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(8888);
if(inet_pton(AF_INET,"127.0.0.1",&addr.sin_addr)<=0)
{
cerr<<"不合法的地址"<<endl;
close(sock);
return 1;
}
if(connect(sock,(sockaddr*)&addr,sizeof(addr))==-1)
{
cerr<<"连接失败"<<endl;
close(sock);
return 1;
}
TcpStream stream(sock);
cout<<"已连接到服务端"<<endl;
string input;
while(true)
{
cout<<"请输入要发送的消息"<<endl;
getline(cin,input);
if(input=="exit")
{
cout<<"已关闭"<<endl;
close(sock);
break;
}
if(!stream.send_message(input))
{
cerr<<"发送失败,请重试"<<endl;
continue;
}
string reply=stream.recv_message();
if(reply.empty())
{
cerr<<"接收反馈失败"<<endl;
continue;
}
cout<<"服务端反馈:"<<reply<<endl;
}
return 0;
}
- 先创建一个客户端的sokcet,随后绑定ip和端口,然后用connect函数连接服务端。
- 然后用深刻的的接口初始化TcpSTream对象,进入while循环,输入要发送的消息回车发送(单独输入exit断开连接),然后接收服务端的反馈。
注意:要先启动服务端再启动客户端。