赞
踩
字符串回响程序类似于 echo
指令,客户端向服务器发送消息,服务器在收到消息后会将消息发送给客户端,该程序实现起来比较简单,同时能很好的体现 socket
套接字编程的流程。
这个程序我们已经基于 UDP
协议实现过了,换成 TCP
协议实现时,程序的结构是没有变化的,同样需要 server.hpp
、server.cc
、client.hpp
、client.cc
这几个文件。
server.hpp 头文件
- #pragma once
-
- #include<iostream>
- #include<sys/types.h>
- #include<sys/socket.h>
- #include<netinet/in.h>
- #include<arpa/inet.h>
- #include "err.hpp"
- #include<cstring>
-
- namespace My_server{
-
- const uint16_t default_port = 8888; // 默认端口号
- class server{
- public:
-
- server(const uint16_t port = default_port)
- :_port(port)
- {}
-
- ~server()
- {}
-
- // 初始化服务器
- void InitServer(){
-
- }
-
- // 启动服务器
- void StartServer()
- {}
-
- private:
- int _sock; // 套接字(存疑)
- uint16_t _port; // 端口号
- };
- }
注意: 这里的 _sock
套接字成员后面需要修改
server.cc 头文件
- //智能指针头文件
- #include<memory>
- #include"server.hpp"
-
- using namespace My_server;
-
- int main(){
-
- std::unique_ptr<server> usvr(new server());
-
- usvr->InitServer();
-
- usvr->StartServer();
-
- return 0;
- }
创建
client.hpp
客户端头文件
- #pragma once
-
- #include<iostream>
- #include<string>
- #include<sys/types.h>
- #include<sys/socket.h>
- #include<netinet/in.h>
- #include<arpa/inet.h>
- #include"err.hpp"
-
- namespace My_client{
-
- class client{
- public:
- client(const std::string& ip,const uint16_t port)
- :server_ip(ip)
- ,server_port(port)
- {}
-
- ~client(){}
-
- //初始化客户端
- void InitClient(){}
-
- // 启动客户端
- void StartClient(){}
-
- private:
- int _sock;// 套接字
- std::string server_ip;//服务器ip
- uint16_t server_port; //服务器端口号
-
- };
- }
创建client.cc文件
- #include"client.hpp"
- #include<memory>
-
- using namespace My_client;
-
- void Usage(char* program){
-
- std::cout<<"Usage : "<<std::endl;
- std::cout<<"\t "<<program<<" ServerIP ServerPort"<<std::endl;
-
- }
-
- int main(int argc,char *argv[]){
-
- if(argc!=3){
- Usage(argv[0]);
- return USAGE_ERR;
- }
-
- //获取服务器IP地址和端口号
- std::string ip(argv[1]);
- uint16_t port=std::stoi(argv[2]);
-
- std::unique_ptr<client> usvr(new client(ip,port));
-
- usvr->InitClient();
- usvr->StartClient();
-
- return 0;
- }
同时需要Makefile文件
- .PHONY:all
- all:server client
-
- server:server.cc
- g++ -o $@ $^ -std=c++14
-
- client:client.cc
- g++ -o $@ $^ -std=c++14
-
- .PHONY:clean
- clean:
- rm -rf server client
和 err.hpp 头文件
- #pragma once
-
- // 错误码
- enum
- {
- USAGE_ERR=1 ,
- SOCKET_ERR,
- BIND_ERR
- };
基于 TCP
协议实现的网络程序也需要 创建套接字、绑定 IP
和端口号
在使用
socket
函数创建套接字时,UDP
协议需要指定参数2为SOCK_DGRAM
,TCP
协议则是指定参数2为SOCK_STREAM
InitServer()
初始化服务器函数 — 位于server.hpp
服务器头文件中的 server
类
- #pragma once
-
- #include<iostream>
- #include<sys/types.h>
- #include<sys/socket.h>
- #include<netinet/in.h>
- #include<arpa/inet.h>
- #include "err.hpp"
- #include<cstring>
-
- namespace My_server{
-
- const uint16_t default_port = 8888; // 默认端口号
- class server{
- public:
-
- server(const uint16_t port = default_port)
- :_port(port)
- {}
-
- ~server()
- {}
-
- // 初始化服务器
- void InitServer(){
-
- //1 创建套接字
- _sock = socket(AF_INET,SOCK_STREAM,0);
- if(_sock==-1){
- //绑定失败
- std::cerr<<"Create Socket Fail!"<<strerror(errno)<<std::endl;
- exit(SOCKET_ERR);
- }
-
- std::cout<<"Create Socket Success!"<<_sock<<std::endl;
- //2 绑定端口号和IP地址
-
- struct sockaddr_in lockal;
- bzero(&lockal,sizeof lockal);
-
- lockal.sin_family = AF_INET;
- lockal.sin_addr.s_addr = INADDR_ANY;
- lockal.sin_port = htons(_port);
-
- if(bind(_sock,(const sockaddr*)&lockal,sizeof(lockal))){
- std::cout<<"Bind IP&&Port Fali:"<<strerror(errno)<<std::endl;
- exit(BIND_ERR);
- }
-
- }
-
- // 启动服务器
- void StartServer()
- {}
-
- private:
- int _sock; // 套接字(存疑)
- uint16_t _port; // 端口号
- };
- }
注意: 在绑定端口号时,一定需要把主机序列转换为网络序列
为什么在绑定端口号阶段需要手动转换为网络序列,而在发送信息阶段则不需要? 这是因为在发送信息阶段,
recvfrom / sendto
等函数会自动将需要发送的信息转换为网络序列,接收信息时同样会将其转换为主机序列,所以不需要手动转换
如果使用的 UDP
协议,那么初始化服务器到此就结束了,但我们本文中使用的是 TCP
协议,这是一个 面向连接 的传输层协议,意味着在初始化服务器时,需要设置服务器为 监听 状态
使用到的函数是 listen
函数
- #include <sys/types.h> /* See NOTES */
- #include <sys/socket.h>
-
- int listen(int sockfd, int backlog);
返回值:监听成功返回 0
,失败返回 -1.
listen函数 使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。
这里的参数2需要设置一个整数,通常为 16、32、64...
,表示 全连接队列 的最大长度,关于 全连接队列 的详细知识放到后续博客中讲解,这里只需要直接使用。
server.hpp
服务器头文件
我们改变一下网络文件的名字
- #pragma once
-
- #include<iostream>
- #include<cerrno>
- #include<sys/types.h>
- #include<sys/socket.h>
- #include<netinet/in.h>
- #include<arpa/inet.h>
- #include "err.hpp"
- #include<cstring>
-
- namespace My_server{
-
- const uint16_t default_port = 8888; // 默认端口号
- const int backlog=32; //全连接队列的最大长度
-
- class server{
- public:
-
- server(const uint16_t port = default_port)
- :_port(port)
- {}
-
- ~server()
- {}
-
- // 初始化服务器
- void InitServer(){
-
- //1 创建套接字
- _listensock = socket(AF_INET,SOCK_STREAM,0);
- if(_listensock==-1){
- //绑定失败
- std::cerr<<"Create Socket Fail!"<<strerror(errno)<<std::endl;
- exit(SOCKET_ERR);
- }
-
- std::cout<<"Create Socket Success!"<<_listensock<<std::endl;
-
- //2 绑定端口号和IP地址
- struct sockaddr_in local;
- bzero(&local,sizeof(local));
- local.sin_family = AF_INET;
- local.sin_addr.s_addr = INADDR_ANY;//绑定任意可用IP地址
- local.sin_port = htons(_port);
-
- if(bind(_listensock,(const sockaddr*)&local,sizeof(local))){
-
- std::cout<<"Bind IP&&Port Fali:"<<strerror(errno)<<std::endl;
- exit(BIND_ERR);
-
- }
-
- //3. 监听
- if(listen(_listensock,backlog) == -1){
- std::cerr << "Listen Fail!" << strerror(errno) << std::endl;
- //新增一个报错
- exit(LISTEN_ERR);
- }
- std::cout<<"Listen Success!"<<std::endl;
-
- }
-
- // 启动服务器
- void StartServer()
- {}
-
- private:
- int _listensock; // 套接字(存疑)
- uint16_t _port; // 端口号
- };
- }
至此基于 TCP
协议 实现的初始化服务器函数就填充完成了,编译并运行服务器,显示初始化服务器成功。
// 示例代码中端口号为8088
TCP
是面向连接,当有客户端发起连接请求时,TCP
服务器需要正确识别并尝试进行连接,当连接成功时,与其进行通信,可使用 accept
函数进行连接。
- #include <sys/types.h> /* See NOTES */
- #include <sys/socket.h>
-
- int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数解读:
sockfd
服务器用于处理连接请求的socket
套接字addr
客户端的sockaddr
结构体信息addrlen
客户端的sockaddr
结构体大写
其中 addr
与 addrlen
是一个 输入输出型 参数,类似于 recvfrom
中的参数。
返回值:连接成功返回一个用于通信的 socket
套接字(文件描述符),失败返回 -1。
这也就意味着之前我们在 TcpServer
中创建的类内成员 sock_
并非是用于通信,而是专注于处理连接请求,在 TCP
服务器中,这种套接字称为 监听套接字
使用 accept
函数处理连接请求
server.hpp
服务器头文件
- #pragma once
-
- #include<iostream>
- #include<cerrno>
- #include<sys/types.h>
- #include<sys/socket.h>
- #include<netinet/in.h>
- #include<arpa/inet.h>
- #include "err.hpp"
- #include<cstring>
-
- namespace My_server{
-
- const uint16_t default_port = 8888; // 默认端口号
- const int backlog=32; //全连接队列的最大长度
-
- class server{
- public:
-
- server(const uint16_t port = default_port)
- :_port(port)
- {}
-
- ~server()
- {}
-
- // 初始化服务器
- void InitServer(){
-
- //1 创建套接字
- _listensock = socket(AF_INET,SOCK_STREAM,0);
- if(_listensock==-1){
- //绑定失败
- std::cerr<<"Create Socket Fail!"<<strerror(errno)<<std::endl;
- exit(SOCKET_ERR);
- }
-
- std::cout<<"Create Socket Success!"<<_listensock<<std::endl;
-
- //2 绑定端口号和IP地址
- struct sockaddr_in local;
- bzero(&local,sizeof(local));
- local.sin_family = AF_INET;
- local.sin_addr.s_addr = INADDR_ANY;//绑定任意可用IP地址
- local.sin_port = htons(_port);
-
- if(bind(_listensock,(const sockaddr*)&local,sizeof(local))){
-
- std::cout<<"Bind IP&&Port Fali:"<<strerror(errno)<<std::endl;
- exit(BIND_ERR);
-
- }
-
- //3. 监听
- if(listen(_listensock,backlog) == -1){
- std::cerr << "Listen Fail!" << strerror(errno) << std::endl;
- //新增一个报错
- exit(LISTEN_ERR);
- }
- std::cout<<"Listen Success!"<<std::endl;
-
- }
-
- // 启动服务器
- void StartServer(){
-
- while(!_quit){
- //1 处理连接请求
- struct sockaddr_in client;
- socklen_t len=sizeof(client);
-
- int sock=accept(_listensock,(struct sockaddr*)&client,&len);
-
- //2 如果连接失败 继续尝试连接
- if(sock==-1){
- std::cerr<<"Accept Fail!"<<strerror(errno)<<std::endl;
- continue;
- }
-
- //连接成功,获取客户端信息
- std::string clientip=inet_ntoa(client.sin_addr);
- uint16_t clientport= ntohs(client.sin_port);
-
- std::cout<<"Server accept"<<clientip + "-"<<clientport<<sock<<" from "<<_listensock<<" success!"<<std::endl;
-
- //3 这里因为是字节流传递,一般而言我们会自己写一个函数
- Service(sock,clientip,clientport);
- }
-
- }
-
- private:
- int _listensock; // 套接字(存疑)
- uint16_t _port; // 端口号
- bool _quit; // 判断服务器是否结束运行
- };
- }
对于 TCP
服务器来说,它是面向字节流传输的,我们之前使用的文件相关操作也是面向字节流,凑巧的是在 Linux
中网络是以挂接在文件系统的方式实现的,种种迹象表明:可以通过文件相关接口进行通信
read
从文件中读取信息(接收消息)write
向文件中写入信息(发送消息)
这两个系统调用的核心参数是 fd
(文件描述符),即服务器与客户端在连接成功后,获取到的 socket
套接字,所以接下来可以按文件操作的套路,完成业务处理
Service()
业务处理函数 — 位于server.hpp
服务器头文件中的 server
类
- void Service(int sock,const std::string& clientip,const uint16_t& clientport){
-
- char buff[1024];
- std::string who=clientip + "-" + std::to_string(clientport);
- while(true){
- //以C语言格式读取,预留'\0'的位置
- ssize_t n = read(sock,buff,sizeof(buff)-1);
- if(n>0){
- //读取成功
- std::cout<<"Server get: "<<buff<<" from "<<who<<std::endl;
- //实际处理可以交给上层逻辑指定
- std::string respond = _func(buff);
- //发送给服务器
- write(sock,buff,strlen(buff));
- }
- else if(n==0){
-
- //表示当前读到了文件末尾,结束读取
- std::cout<<"Client "<<who<<" "<<sock<<" quit!"<<std::endl;
- close(sock);
- break;
- }
- else{
- // 读取出问题(暂时)
- std::cerr << "Read Fail!" << strerror(errno) << std::endl;
- close(sock); // 关闭文件描述符
- break;
- }
- }
- }
为了更好的实现功能解耦,这里将真正的业务处理函数交给上层处理,编写完成后传给 TcpServer
对象即可,当然,在 TcpServer
类中需要添加对应的类型
这里设置回调函数的返回值为
string
,参数同样为string
using func_t = std::function<std::string(std::string)>;
server.hpp
服务器头文件
- #pragma once
-
- #include<iostream>
- #include<cerrno>
- #include<sys/types.h>
- #include<sys/socket.h>
- #include<netinet/in.h>
- #include<arpa/inet.h>
- #include "err.hpp"
- #include<cstring>
- #include <unistd.h>
- #include<functional>
-
- namespace My_server{
-
- const uint16_t default_port = 8888; // 默认端口号
- const int backlog=32; //全连接队列的最大长度
-
- using func_t = std::function<std::string(std::string)>;
- class server{
- public:
-
- server(const func_t &func,const uint16_t port = default_port)
- :_func(func)
- ,_port(port)
- ,_quit(false)
- {}
-
- ~server()
- {}
-
- // 初始化服务器
- void InitServer(){
-
- //1 创建套接字
- _listensock = socket(AF_INET,SOCK_STREAM,0);
- if(_listensock==-1){
- //绑定失败
- std::cerr<<"Create Socket Fail!"<<strerror(errno)<<std::endl;
- exit(SOCKET_ERR);
- }
-
- std::cout<<"Create Socket Success!"<<_listensock<<std::endl;
-
- //2 绑定端口号和IP地址
- struct sockaddr_in local;
- bzero(&local,sizeof(local));
- local.sin_family = AF_INET;
- local.sin_addr.s_addr = INADDR_ANY;//绑定任意可用IP地址
- local.sin_port = htons(_port);
-
- if(bind(_listensock,(const sockaddr*)&local,sizeof(local))){
-
- std::cout<<"Bind IP&&Port Fali:"<<strerror(errno)<<std::endl;
- exit(BIND_ERR);
-
- }
-
- //3. 监听
- if(listen(_listensock,backlog) == -1){
- std::cerr << "Listen Fail!" << strerror(errno) << std::endl;
- //新增一个报错
- exit(LISTEN_ERR);
- }
- std::cout<<"Listen Success!"<<std::endl;
-
- }
-
- // 启动服务器
- void StartServer(){
-
- while(!_quit){
- //1 处理连接请求
- struct sockaddr_in client;
- socklen_t len=sizeof(client);
-
- int sock=accept(_listensock,(struct sockaddr*)&client,&len);
-
- //2 如果连接失败 继续尝试连接
- if(sock==-1){
- std::cerr<<"Accept Fail!"<<strerror(errno)<<std::endl;
- continue;
- }
-
- //连接成功,获取客户端信息
- std::string clientip=inet_ntoa(client.sin_addr);
- uint16_t clientport= ntohs(client.sin_port);
-
- std::cout<<"Server accept"<<clientip + "-"<<clientport<<sock<<" from "<<_listensock<<" success!"<<std::endl;
-
- //3 这里因为是字节流传递,一般而言我们会自己写一个函数
- Service(sock,clientip,clientport);
- }
- }
-
- void Service(int sock,const std::string& clientip,const uint16_t& clientport){
-
- char buff[1024];
- std::string who=clientip + "-" + std::to_string(clientport);
- while(true){
- //以C语言格式读取,预留'\0'的位置
- ssize_t n = read(sock,buff,sizeof(buff)-1);
- if(n>0){
- //读取成功
- std::cout<<"Server get: "<<buff<<" from "<<who<<std::endl;
- //实际处理可以交给上层逻辑指定
- std::string respond = _func(buff);
- //发送给服务器
- write(sock,buff,strlen(buff));
- }
- else if(n==0){
-
- //表示当前读到了文件末尾,结束读取
- std::cout<<"Client "<<who<<" "<<sock<<" quit!"<<std::endl;
- close(sock);
- break;
- }
- else{
- // 读取出问题(暂时)
- std::cerr << "Read Fail!" << strerror(errno) << std::endl;
- close(sock); // 关闭文件描述符
- break;
- }
- }
- }
-
- private:
- int _listensock; // 套接字(存疑)
- uint16_t _port; // 端口号
- bool _quit; // 判断服务器是否结束运行
- func_t _func;// 回调函数
- };
- }
对于当前的 TCP
网络程序(字符串回响)来说,业务处理函数逻辑非常简单,无非就是直接将客户端发送过来的消息,重新转发给客户端
server.cc
服务器源文件
- //智能指针头文件
- #include<memory>
- #include"server.hpp"
- #include<string>
-
- using namespace My_server;
- // 业务处理回调函数(字符串回响)
- std::string echo(std::string request){
- return request;
- }
-
- int main(){
-
- std::unique_ptr<server> usvr(new server(echo));
-
- usvr->InitServer();
-
- usvr->StartServer();
-
- return 0;
- }
尝试编译并运行服务器,可以看到当前 bash
已经被我们的服务器程序占用了,重新打开一个终端,并通过 netstat
命令查看网络使用情况(基于 TCP
协议)
对于客户端来说,服务器的 IP
地址与端口号是两个不可或缺的元素,因此在客户端类中, server_ip
和 server_port
这两个成员是少不了的,当然得有 socket
套接字
初始化客户端只需要干一件事:创建套接字,客户端是主动发起连接请求的一方,也就意味着它不需要使用 listen
函数设置为监听状态
注意: 客户端也是需要 bind
绑定的,但不需要自己手动绑定,由操作系统帮我们自动完成
client.hpp
客户端头文件
- #pragma once
-
- #include<iostream>
- #include<string>
- #include<sys/types.h>
- #include<sys/socket.h>
- #include<netinet/in.h>
- #include<arpa/inet.h>
- #include"err.hpp"
- #include<cerrno>
- #include<cstring>
-
- namespace My_client{
-
- class client{
- public:
- client(const std::string& ip,const uint16_t port)
- :server_ip(ip)
- ,server_port(port)
- {}
-
- ~client(){}
-
- //初始化客户端
- void InitClient(){
-
- // 1. 创建套接字
- _sock = socket(AF_INET, SOCK_STREAM, 0);
- if (_sock == -1)
- {
- std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
- exit(SOCKET_ERR);
- }
- std::cout << "Create Sock Succeess! " << _sock << std::endl;
-
-
- }
-
- // 启动客户端
- void StartClient(){}
-
- private:
- int _sock;// 套接字
- std::string server_ip;//服务器ip
- uint16_t server_port; //服务器端口号
-
- };
- }
因为 TCP
协议是面向连接的,服务器已经处于处理连接请求的状态了,客户端现在需要做的就是尝试进行连接,使用 connect
函数进行连接。
- #include <sys/types.h> /* See NOTES */
- #include <sys/socket.h>
-
- int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解读:
sockfd
需要进行连接的套接字addr
服务器的sockaddr
结构体信息addrlen
服务器的sockaddr
结构体大小
返回值:连接成功返回 0
,连接失败返回 -1.
在连接过程中,可能遇到很多问题,比如 网络传输失败、服务器未启动 等,这些问题的最终结果都是客户端连接失败,如果按照之前的逻辑(失败就退出),那么客户端的体验感会非常不好,因此在面对连接失败这种常见问题时,客户端应该尝试重连,如果重连数次后仍然失败,才考虑终止进程
注意: 在进行重连时,可以使用 sleep()
等函数使程序睡眠一会,给网络恢复留出时间
StartClient()
启动客户端函数 — 位于client.hpp
中的client
类.
- #pragma once
-
- #include<iostream>
- #include<string>
- #include<sys/types.h>
- #include<sys/socket.h>
- #include<netinet/in.h>
- #include<arpa/inet.h>
- #include"err.hpp"
- #include<cerrno>
- #include<cstring>
- #include<unistd.h>
- namespace My_client{
-
- class client{
- public:
- client(const std::string& ip,const uint16_t port)
- :server_ip(ip)
- ,server_port(port)
- {}
-
- ~client(){}
-
- //初始化客户端
- void InitClient(){
-
- // 1. 创建套接字
- _sock = socket(AF_INET, SOCK_STREAM, 0);
- if (_sock == -1)
- {
- std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
- exit(SOCKET_ERR);
- }
- std::cout << "Create Sock Succeess! " << _sock << std::endl;
-
-
- }
-
- // 启动客户端
- void StartClient(){
-
- //填充服务器的sockaddr_int 结构体信息
- struct sockaddr_in server;
- socklen_t len=sizeof(server);
- bzero(&server,len);
- server.sin_family = AF_INET;
- inet_aton(server_ip.c_str(), &server.sin_addr); // 将点分十进制转化为二进制IP地址的另一种方法
- server.sin_port = htons(server_port);
-
- //尝试重连五次
- int n=5;
- while(n){
- int ret = connect(_sock,(const struct sockaddr*)&server,len);
- if(ret==0){
- // 连接成功,可以跳出循环
- break;
- }
- // 尝试进行重连
- std::cerr << "网络异常,正在进行重连... 剩余连接次数: " << --n << std::endl;
- sleep(1);
- }
-
- // 如果剩余重连次数为 0,证明连接失败
- if(n == 0)
- {
- std::cerr << "连接失败! " << strerror(errno) << std::endl;
- close(_sock);
- exit(CONNECT_ERR);//新加错误标识符
-
- }
-
- // 连接成功
- std::cout << "连接成功!" << std::endl;
-
- // 进行业务处理
- // Service();
- }
-
- private:
- int _sock;// 套接字
- std::string server_ip;//服务器ip
- uint16_t server_port; //服务器端口号
-
- };
- }
客户端在进行业务处理时,同样可以使用 read
和 write
进行网络通信
Service()
业务处理函数 — 位于client.hpp
客户端头文件中的TcpClient
类
- // 业务处理
- void Service()
- {
- char buff[1024];
- std::string who = server_ip + "-" + std::to_string(server_port);
- while(true)
- {
- // 由用户输入信息
- std::string msg;
- std::cout << "Please Enter >> ";
- std::getline(std::cin, msg);
-
- // 发送信息给服务器
- write(_sock, msg.c_str(), msg.size());
- // 接收来自服务器的信息
- ssize_t n = read(_sock, buff, sizeof(buff) - 1);
- if(n > 0)
- {
- // 正常通信
- buff[n] = '\0';
- std::cout << "Client get: " << buff << " from " << who << std::endl;
- }
- else if(n == 0)
- {
- // 读取到文件末尾(服务器关闭了)
- std::cout << "Server " << who << " quit!" << std::endl;
- close(_sock); // 关闭文件描述符
- break;
- }
- else
- {
- // 读取异常
- std::cerr << "Read Fail!" << strerror(errno) << std::endl;
- close(_sock); // 关闭文件描述符
- break;
- }
- }
- }
代码示例:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。