Linux -- 初步了解 TCP 编程
目录
TCP 协议
TCP 编程流程图
listen 函数(监听连接请求)
accept 函数(等待并接收连接)
connect 函数(建立连接)
shutdown 函数
gitee
主要代码
TcpServer.hpp
如何让服务器一次处理多个客户端的请求?
version 多进程:
version 多线程:
version 线程池:
完整代码:
MainClient.cc
运行结果
version 多进程:
version 多线程:
version 线程池:
TCP 协议
TCP(传输控制协议,Transmission Control Protocol)是互联网协议套件中的核心协议之一,它提供了面向连接、可靠的字节流服务。
因为 TCP 是面向连接的协议,在客户端和服务器通信前,需要先建立连接。
TCP 编程流程图
listen 函数(监听连接请求)
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd
是想要设置为监听模式的套接字描述符。这个描述符是由之前的socket()
系统调用返回的。
backlog
参数指定了操作系统可以为该套接字排队的最大连接请求数。它是一个建议性的最大值,实际的最大长度可能会由操作系统限定。
- 当
listen()
成功执行时,它会返回 0;- 如果发生错误,则返回 -1,并且会设置全局变量
errno
来指示具体的错误类型。
当
listen
被调用后,套接字就进入了监听状态,并开始排队连接请求。一旦有新的连接请求到达,它会被加入到队列中,直到服务器程序调用accept
函数来处理这些连接请求。
accept 函数(等待并接收连接)
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd
:这是由之前的socket()
系统调用创建,并通过bind()
和listen()
设置为监听模式的套接字描述符。
addr
:这是一个指向struct sockaddr
结构的指针,用于返回已连接客户端的地址信息。如果不需要客户端地址信息,可以将此参数设置为NULL
。是输出型参数。
addrlen
:这是一个指向socklen_t
类型变量的指针,该变量在调用时应包含addr
指向结构的大小(以字节为单位)。调用后,它会被更新为实际存储在addr
中的地址长度。如果addr
是NULL
,那么这个参数也可以是NULL
。是输出型参数。
返回值:
- 如果成功,
accept
返回一个新的文件描述符,这个描述符代表与客户端之间的连接。服务器程序可以通过这个新描述符与客户端通信。- 如果有错误发生,
accept
返回 -1,并设置全局变量errno
以指示错误类型。
accept
函数会阻塞直到有一个新的连接建立。当有连接到达并且操作系统将其加入到队列中之后,accept
就会返回一个新连接的文件描述符,而原来的监听套接字sockfd
仍然保持监听状态,可以继续接收更多的连接请求。如何理解监听套接字 sockfd 和 accept 的返回值之间的关系呢?
一般的餐饮店门口会有一个揽客的,店内也会有服务员,揽客的揽到客人之后,由服务员为客人提供服务,揽客的继续在店门口揽客,并不会做服务员的工作,服务员也只做服务员的工作,不会到门口揽客,两个人各司其职。揽客的就是监听套接字 sockfd,服务员就是 accept 的返回值,sockfd 接收到连接后,就继续监听,accept 的返回值会为通信提供服务。
connect 函数(建立连接)
connect
函数是用于建立客户端与服务器之间的连接的系统调用或库函数。它主要用于 TCP(传输控制协议)套接字,但也可以用于其他类型的面向连接的协议。当一个程序需要主动发起一个网络连接到远程服务时,就会使用 connect
函数。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
是一个整数,表示之前通过socket()
系统调用创建的未连接套接字描述符。
addr
是一个指向sockaddr
结构的指针,该结构包含了要连接的服务器地址信息。通常你会使用sockaddr_in
或sockaddr_in6
来具体指定 IPv4 或 IPv6 地址及端口。
addrlen
是上述地址结构的大小,以字节为单位。
返回值:
- 如果成功,
connect
返回 0。- 如果发生错误,则返回 -1,并设置
errno
变量以指示错误类型。
shutdown 函数(关闭套接字描述符)
用完的套接字描述符必须关掉,因为描述符的数量是有限的,用完不关掉会导致描述符泄露。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd
是一个整数,表示要关闭的套接字描述符。
how
是一个整数,指定了如何关闭套接字,它可以取以下值之一:
SHUT_RD
:不再接收数据。对于双向套接字,这会阻止进一步的数据接收。SHUT_WR
:不再发送数据。对于双向套接字,这会发送一个FIN包给对端,表明发送方已经完成发送,并且不会再发送更多数据。SHUT_RDWR
:不再接收也不再发送数据。这是组合了前两种情况的效果,等价于分别调用SHUT_RD
和SHUT_WR
。
返回值:
- 如果成功,
shutdown
返回 0。- 如果发生错误,则返回 -1,并设置
errno
变量以指示错误类型。
gitee
tcp_echo_server · zihuixie/Linux_Learning - 码云 - 开源中国https://gitee.com/zihuixie/linux_-learning/tree/master/tcp_echo_server
主要代码
TcpServer.hpp
如何让服务器一次处理多个客户端的请求?
version 多进程:
为什么创建子进程?
创建子进程后,父进程继续接收连接,子进程则处理通信任务,提供服务,就可以实现一次处理多个请求。
为什么关掉描述符?
创建子进程时,父进程就把 accept 的返回值 sockfd 交给了子进程。
父进程如果不关掉 sockfd,会导致可用的 套接字描述符 越来越少,而且父进程不需要用到 sockfd,父进程只需要监听。子进程也不需要监听,所以子进程关掉 _listensock。
由于进程的独立性,子进程 关掉 _listensock 并不会影响 父进程继续监听,父进程关掉 sockfd 并不会影响子进程处理通信任务。
为什么创建孙子进程?
如果父进程等待子进程的执行,那么父进程需要等待 子进程 处理完当前的任务才可以继续接收连接,那么服务器还是一次只能处理一个请求,所以需要分离父子进程。
创建孙子进程,就可以解决服务器一次处理多个请求!
创建孙子进程后,子进程退出,那么孙子进程变成僵尸进程,由系统收养,父进程和子进程都不需要关心孙子进程的执行,父进程可以继续接收连接。
//创建子进程
pid_t id = fork();
if (id == 0) // 子进程
{
close(_listensock);//子进程不需要监听
//创建孙子进程
if (fork() > 0)
exit(0); // 子进程退出
//孙子进程变僵尸进程,由系统收养
Service(sockfd, InetAddr(peer)); // 孙子进程执行任务
exit(0);
}
// 父进程
close(sockfd);//父进程不需要提供服务
waitpid(id,nullptr,0);
version 多线程:
注意线程不需要关掉描述符,因为线程间共享描述符表!
pthread_t t;
ThreadData *data =new ThreadData(sockfd,InetAddr(peer),this);
pthread_create(&t,nullptr,HandlerSock,data);
version 线程池:
多线程版本中是客户端请求连接后再创建线程,线程池是先创建线程,客户端请求连接时,服务器可以直接使用线程处理请求。
task_t t=std::bind(&TcpServer::Service,this,sockfd,InetAddr(peer));
ThreadPool<task_t>::GetInstance()->Enqueue(t);
完整代码:
#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"
const static int gbacklog = 16;
const static int defaultsockfd = -1;
using task_t = std::function<void()>;
enum
{
SOCKET_ERROR = 1,
BIND_ERROR,
LISTEN_ERROR
};
class TcpServer;
class ThreadData
{
public:
ThreadData(int fd,InetAddr addr,TcpServer *s)
:_sockfd(fd),_ClientAddr(addr),_self(s)
{}
public:
int _sockfd;
InetAddr _ClientAddr;
TcpServer *_self;
};
class TcpServer
{
public:
TcpServer(uint16_t port)
: _port(port), _isrunning(false), _listensock(defaultsockfd)
{
}
~TcpServer()
{
if (_listensock > defaultsockfd)
{
close(_listensock); // 关闭监听
}
}
void InitServer()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0); // TCP协议用SOCK_STREAM
// 创建失败
if (_listensock < 0)
{
LOG(FATAL, "socket error\n");
exit(SOCKET_ERROR);
}
LOG(DEBUG, "socket create success, sockfd: %d\n", _listensock);
// 填地址
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(_port);
// 绑定
int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local));
// 绑定失败
if (n < 0)
{
LOG(FATAL, "%d bind error\n", _port);
exit(BIND_ERROR);
}
LOG(DEBUG, "bind success,sockfd: %d\n", _listensock);
// 开始监听连接
n = listen(_listensock, gbacklog);
// 监听失败
if (n < 0)
{
LOG(FATAL, "listen error\n");
exit(LISTEN_ERROR);
}
LOG(DEBUG, "listen success,sockfd: %d\n", _listensock);
}
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()) + "]#";
// 缓冲区
char inbuffer[1024];
while (true)
{
// 读取字节流
// n:读到的字节数
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");
break;
}
}
// 服务器开始退出
std::cout << "server start to quit..." << std::endl;
// sockfd 才是提供服务的套接字描述符,_listensock 用于监听
// 需要关掉提供提供服务的 sockfd
shutdown(sockfd, SHUT_RD);
std::cout << "shut_rd" << std::endl;
}
static void* HandlerSock(void *args)
{
pthread_detach(pthread_self());//分离新、主线程
ThreadData *td=static_cast<ThreadData*>(args);
td->_self->Service(td->_sockfd,td->_ClientAddr);
delete td;
return nullptr;
}
// 循环接收连接
void Loop()
{
_isrunning = true;
while (_isrunning)
{
// 输出型参数
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 等待并接收连接
int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
// 接收失败
if (sockfd < 0)
{
LOG(WARNING, "accept error\n");
// 本次接收失败之后还可以继续接收
continue;
}
// 开始执行任务
// version 0: 一次只能处理一个请求
// Service(sockfd,InetAddr(peer));
// version 1:用多进程
// // 创建子进程
// pid_t id = fork();
// if (id == 0) // 子进程
// {
// close(_listensock);//子进程不需要监听
// // 创建孙子进程
// if (fork() > 0)
// exit(0); // 子进程退出
// //孙子进程变僵尸进程,由系统收养
// Service(sockfd, InetAddr(peer)); // 孙子进程执行任务
// exit(0);
// }
// // 父进程
// close(sockfd);//父进程不需要提供服务
// waitpid(id,nullptr,0);
// // version 2:用多线程
// pthread_t t;
// ThreadData *data =new ThreadData(sockfd,InetAddr(peer),this);
// pthread_create(&t,nullptr,HandlerSock,data);
// version 3:用线程池
task_t t=std::bind(&TcpServer::Service,this,sockfd,InetAddr(peer));
ThreadPool<task_t>::GetInstance()->Enqueue(t);
}
_isrunning = false;
}
private:
int _listensock; // 用于监听
uint16_t _port;
bool _isrunning; // 是否正在运行
};
MainClient.cc
#include <iostream>
#include <sys/types.h>
#include <string>
#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" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
uint16_t serverport = std::stoi(argv[2]);
std::string serverip = argv[1];
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
struct sockaddr_in server;
memset(&server, 0, sizeof(struct sockaddr_in));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverip.c_str());
server.sin_port = htons(serverport);
// 客户端不需要调用bind 函数
// 客户端与服务器建立连接
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
// 连接建立失败
if (n < 0)
{
std::cerr<<"connect error"<<std::endl;
exit(3);
}
while(true)
{
//1、输入消息
std::cout<<"\nPlease Enter# ";
std::string message;
std::getline(std::cin,message);
//2、发送消息
ssize_t s=send(sockfd,message.c_str(),message.size(),0);
if(s>0)
{
//3、接收应答
char inbuffer[1024];
ssize_t r=recv(sockfd,inbuffer,sizeof(inbuffer)-1,0);
//接收成功
if(r>0)
{
inbuffer[r]=0;
std::cout<<inbuffer<<std::endl;
}
//接收失败
else
{
break;
}
}
else
{
break;
}
}
shutdown(sockfd,SHUT_WR);
return 0;
}
运行结果
有 不同的端口号 可以看出服务器可以一次处理多个请求!
version 多进程:
version 多线程:
version 线程池: