目录
前言:
1、InitServer类的实现
1.1. 创建流式套接字
1.2. bind 绑定一个固定的网络地址和端口号
1.3.listen监听机制
1.4.完整代码
2. 循环接收接口与服务接口
2.1.accept函数讲解
讲个商场拉客的故事方便我们理解:
2.2.服务接口实现
3.服务端和客户端的实现
3.1.服务端
3.2.客户端
connect函数讲解:
4.多版本测试运行
4.1.单进程处理
4.2.多进程处理
测试结果:
4.3.多线程处理
代码:编辑
前言:
我们上一篇文章讲解了利用udp协议实现一个简单的echo_server程序,将客户端的数据在服务端打印出来UDP协议。今天我们来讲解利用TCP协议实现的一个简单的echo_server程序,认识并熟悉各个接口的功能以及如何使用!
1、InitServer类的实现
1.1. 创建流式套接字
socket函数讲解:
- socket()打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描述符;
- 应用程序可以像读写文件一样用 read/write 在网络上收发数据;
- 如果 socket()调用出错则返回-1;
- 对于 IPv4, family 参数指定为 AF_INET;
- 对于 TCP 协议,type 参数指定为 SOCK_STREAM, 表示面向流的传输协议
- protocol 参数的介绍从略,指定为 0 即可。
该部分与udp唯一的区别就是type参数的不同,因为对于TCP协议是面向流的传输协议,所以参数指定为SOCK_STREAM。
代码:
1.2. bind 绑定一个固定的网络地址和端口号
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用 bind 绑定一个固定的网络地址和端口号;
- bind()成功返回 0,失败返回-1。
- bind()的作用是将参数 sockfd 和 myaddr 绑定在一起, 使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号;
- struct sockaddr *是一个通用指针类型,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen指定结构体的长度;
注意这部分代码与udp是一模一样的。
代码:
1.3.listen监听机制
listen函数讲解
- listen()声明 sockfd 处于监听状态, 并且最多允许有 backlog 个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是 5)
- listen()成功返回 0,失败返回-1;
tcp是面向连接的,所以通信之前,必须先建立连接。服务器是被连接的。tcpserver 启动,未来首先要一直等待客户的连接到来,所以需要listen函数进行监听。
代码:
1.4.完整代码
void InitServer()
{
// 1. 创建流式套接字
_listensock = ::socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
LOG(FATAL, "socket error");
exit(SOCKET_ERROR);
}
LOG(DEBUG, "socket create success, sockfd is : %d\n", _listensock);
// 2. bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
int n = ::bind(_listensock, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(FATAL, "bind error");
exit(BIND_ERROR);
}
LOG(DEBUG, "bind success, sockfd is : %d\n", _listensock);
// 3. tcp是面向连接的,所以通信之前,必须先建立连接。服务器是被连接的
// tcpserver 启动,未来首先要一直等待客户的连接到来
n = ::listen(_listensock, gbacklog);
if (n < 0)
{
LOG(FATAL, "listen error");
exit(LISTEN_ERROR);
}
LOG(DEBUG, "listen success, sockfd is : %d\n", _listensock);
}
2. 循环接收接口与服务接口
Loop()循环循环接收接口需要:
- 不断从套接字文件中accept获取连接流与客户端信息!
- 获取成功后,就可以进行服务了
- 服务就是从流中读取数据,然后处理之后再写回流中!!!使用的接口是read与write,文件流中我们对他们很熟悉!!!
2.1.accept函数讲解
- 三次握手完成后, 服务器调用 accept()接受连接;
- 如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr 是一个传出参数,accept()返回时传出客户端的地址和端口号;
- 如果给 addr 参数传 NULL,表示不关心客户端的地址;
accept返回的套接字和传入的参数套接字到底有什么关系?
传入的套接字这个参数不是真正参与通信的,只是用来建立连接的。我们真正用来与客户端通信的是返回的套接字!
讲个商场拉客的故事方便我们理解:
就比如我们在商场逛街,很多门面前面有拉客的,拉客的人在马路上拉客,然后接到客人之后,会从店里再喊一个服务员用来服务新拉过来的客人。因此拉客的人就是我们的参数,而返回值的套接字才是真正服务我们的,也就是说如果有多个客人,就会有很多个返回值的套接字,分别用来服务。
部分代码:
2.2.服务接口实现
服务就是从流中读取数据,然后处理之后再写回流中。使用的接口是read与write。
void Service(int sockfd, InetAddr client)
{
LOG(DEBUG, "get a new link, info %s:%d, fd : %d\n", client.Ip().c_str(), client.Port(), sockfd);
std::string clientaddr = "[" + client.Ip() + ":" + std::to_string(client.Port()) + "]# ";
while (true)
{
char inbuffer[1024];
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
if (n > 0)
{
inbuffer[n] = 0;
std::cout << clientaddr << inbuffer << std::endl;
std::string echo_string = "[server echo]# ";
echo_string += inbuffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
// client 退出&&关闭连接了
LOG(INFO, "%s quit\n", clientaddr.c_str());
break;
}
else
{
LOG(ERROR, "read error\n", clientaddr.c_str());
break;
}
}
std::cout << "server开始退出" << std::endl;
shutdown(sockfd, SHUT_RD);
std::cout << "shut _ rd " << std::endl;
sleep(10);
//shutdown(sockfd, SHUT_WR);
//std::cout << "shut _ wr " << std::endl;
//::close(sockfd); // 文件描述符泄漏
}
3.服务端和客户端的实现
3.1.服务端
服务端简单的创建一个服务器类然后进行初始化和loop就可以了。
代码:
#include "TcpServer.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " local_port\n" << std::endl;
}
// ./tcpserver port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
EnableScreen();
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
tsvr->InitServer();
tsvr->Loop();
return 0;
}
3.2.客户端
- 首先根据传入的参数进行初始化服务器IP地址和端口号
- 然后创建套接字文件 ,并进行connect连接绑定bind,客户端回被动绑定一个端口号。
- 绑定成功之后就可以通过sockfd进行写入与读取了。
connect函数讲解:
- 客户端需要调用 connect()连接服务器;
- connect 和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址, 而connect 的参数是对方的地址;
- connect()成功返回 0,出错返回-1;
代码:
#include <iostream>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< std::endl;
}
// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// tcp client 要bind,不要显示的bind.
struct sockaddr_in server;
// 构建目标主机的socket信息
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());
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
std::cerr << "connect error" << std::endl;
exit(3);
}
while(true)
{
std::cout << "Please Enter# ";
std::string outstring;
std::getline(std::cin, outstring);
ssize_t s = send(sockfd, outstring.c_str(), outstring.size(), 0); //write
if(s > 0)
{
char inbuffer[1024];
ssize_t m = recv(sockfd, inbuffer, sizeof(inbuffer)-1, 0);
if(m > 0)
{
inbuffer[m] = 0;
std::cout << inbuffer<< std::endl;
}
else
{
break;
}
}
else
{
break;
}
}
shutdown(sockfd, SHUT_WR);
//::close(sockfd);
return 0;
}
4.多版本测试运行
4.1.单进程处理
一次只能处理一个请求,显然这个版本是很废的,根本不满足用户需求,直接跳过
测试结果:
4.2.多进程处理
子进程会继承父进程的文件描述符表,所以子进程一定会看到父进程之前创建的sockfd,父子进程的文件描述符表是独立的,子进程会拷贝父进程的那一份,线程才会共享
- 对于子进程:关心sockfd, 不关心listensock,所以对于子进程需要关闭listensock
- 对于父进程:关心listensock,不关心sockfd,所以对于父进程需要关闭sockfd
为什么父进程要关闭sockfd,不然一直创建会导致sockfd一直减少,浪费资源
子进程还会创建子进程,就是孙子进程,我们不进行任何处理,那么这个孙子进程就变成了孤儿进程,系统自己领养进行运行,而父进程仍在源源不断的创建新的子进程,那么这个版本服务器就可以并发处理了!
代码:
// Version 1: 采用多进程
pid_t id = fork();
if (id == 0)
{
// child : 关心sockfd, 不关心listensock
::close(_listensock); // 建议
if(fork() > 0) exit(0);
Service(sockfd, InetAddr(peer)); //孙子进程 -- 孤儿进程 --- 系统领养
exit(0);
}
// father: 关心listensock,不关心sockfd
::close(sockfd);
waitpid(id, nullptr, 0);
测试结果:
测试成功!但是我们知道切换进程时,CPU会切换上下文和热点数据。在并发场景下多进程的不断切换会消耗大量的性能!
而作为轻量级进程的线程就可以避免这样的问题!
4.3.多线程处理
我们采用线程分离的方法来实现并发处理!
多线程禁止关闭文件描述符!因为多线程是共享文件描述符表的,如果直接将sockfd关掉了,那么先创建的线程可能无法通过文件描述符来读取数据。
在服务器编程中,为了处理大量的客户端请求,可以采用多线程技术来提高服务器的并发性能。在这种情况下,线程分离可以确保每个子线程在结束后自动释放资源,从而避免资源泄露,提高服务器的稳定性和性能。
代码:
测试结果:非常nice!