????C++专栏: 南桥谈C++
????C语言专栏: C语言学习系列
????Linux学习专栏: 南桥谈Linux
????数据结构学习专栏: 数据结构杂谈
????数据库学习专栏: 南桥谈MySQL
????Qt学习专栏: 南桥谈Qt
????菜鸡代码练习: 练习随想记录
????git学习: 南桥谈Git
前言
在学习本章之前,先看【Linux网络编程】Socket编程–UDP:实现服务器接收客服端的消息 | DictServer简单的英译汉的网络字典 | 简单聊天室】,里面详细介绍函数的使用方法,小编在这篇文章不再具体介绍。
TCP echo server
服务端
创建套接字 | 绑定套接字
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
在TCP中,第二个参数,指定套接字类型应该为SOCK_STREAM
,其余的和UDP中一样。
绑定套接字:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
将创建的套接字sockfd
与struct addr
绑定。
//创建
_listensockfd=::socket(AF_INET,SOCK_STREAM,0);
if(_listensockfd<0)
{
exit(SOCKET_ERROR);
LOG(FATAL,"socket create error\n");
}
LOG(INFO,"socket create sussedd,listensockfd: %d\n",_listensockfd); //3
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;
//绑定:sockfd 和 socket addr
if(::bind(_listensockfd,(struct sockaddr*)&local,sizeof(local))<0)
{
LOG(FATAL,"bind error\n");
exit(BAND_ERROR);
}
listen–设置监听状态
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
-
int sockfd
创建的套接字 -
int backlog
是连接队列的长度
由于TCP是面向连接的,因此TCP需要不断地能够做到获取连接,所以设置成监听状态,让套接字准备好,随时准备等待别人来连网。
const static int gblcklog=8;
if(::listen(_sockfd,gblcklog)<0)
{
LOG(FATAL,"listen error\n");
exit(LISTEN_ERROR);
}
accetp–获取连接
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
int sockfd
设置为listen
状态的套接字 - 后面俩输出型参数用来获取 client 端的套接字信息
- 返回值:返回值是一个文件描述符
如何理解这里的文件描述符和_sockfd
?
-
_sockfd
套接字是用来获取新的连接,accept
返回的文件描述符套接字是用来给客户提供服务的,随这_sockfd
套接字获取的连接增多,accept
返回的文件描述符套接字会越来越多。 -
_sockfd
称之为监听套接字
while(_isrunning)
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
// 获取新连接
int sockfd=::accept(_listensockfd,(struct sockaddr*)&client,&len);
if(sockfd<0)
{
//获取连接失败
LOG(WARING,"accept error\n");
continue;
}
InetAddr addr(client);
LOG(INFO,"get a new link,client info: %s\n",addr.AddrStr().c_str());
//提供服务
Service(sockfd,addr);
read–读取数据 | write–读取数据
TCP是面向字节流的,符合流式的特性,在Linux以及C++中,学过文件流等流式特性,这些都属于文件。UDP无法直接做文件读取,需要使用sento
、recvdrom
。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
服务端提供信息version–0(单进程版)
#pragma once
#include<iostream>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"Log.hpp"
#include"InetAddr.hpp"
using namespace log_ns;
enum
{
SOCKET_ERROR=1,
BAND_ERROR,
LISTEN_ERROR
};
const static int gport=8888;
const static int gsock=-1;
const static int gblcklog=8;
class TcpServer
{
public:
TcpServer(uint16_t port=gport)
:_port(port)
,_listensockfd(gsock)
,_isrunning(false)
{}
void InitServer()
{
//创建
_listensockfd=::socket(AF_INET,SOCK_STREAM,0);
if(_listensockfd<0)
{
exit(SOCKET_ERROR);
LOG(FATAL,"socket create error\n");
}
LOG(INFO,"socket create sussess,listensockfd: %d\n",_listensockfd); //3
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;
//绑定:sockfd 和 socket addr
if(::bind(_listensockfd,(struct sockaddr*)&local,sizeof(local))<0)
{
LOG(FATAL,"bind error\n");
exit(BAND_ERROR);
}
LOG(INFO,"bind success\n");
// 设置监听状态 listen
if(::listen(_listensockfd,gblcklog)<0)
{
LOG(FATAL,"listen error\n");
exit(LISTEN_ERROR);
}
LOG(INFO,"listen success\n");
}
void Loop()
{
_isrunning=true;
while(_isrunning)
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
// 获取新连接
int sockfd=::accept(_listensockfd,(struct sockaddr*)&client,&len);
if(sockfd<0)
{
//获取连接失败
LOG(WARING,"accept error\n");
continue;
}
InetAddr addr(client);
LOG(INFO,"get a new link,client info: %s\n",addr.AddrStr().c_str());
//提供服务--version 0
Service(sockfd,addr);
}
_isrunning=false;
}
//提供服务--version 0
void Service(int sockfd,InetAddr addr)
{
//长服务
while(true)
{
char inbuffer[1024];
ssize_t n=::read(sockfd,inbuffer,sizeof(inbuffer)-1);
if(n>0)
{
std::string echo_string="[sever echo] # ";
echo_string+=inbuffer;
write(sockfd,echo_string.c_str(),echo_string.size());
}
else if(n==0)
{
LOG(INFO,"client %s quit\n",addr.AddrStr().c_str());
break;
}
else
{
LOG(ERROR,"read error: %s\n",addr.AddrStr().c_str());
break;
}
}
::close(sockfd);
}
~TcpServer()
{}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning;
};
服务端提供信息version–1(多进程版)
在上一个版本中,服务器只能接受一个客户端发来的连接,无法接受多个客户端发来的请求,因此称之为单进程版。在这个版本中,使用多进程,实现多个客户端都可以向服务器发起连接。
上述父进程在创建子进程后,将父进程的数据结构以及文件描述符表都拷贝给了子进程,此时父子进程都指向同样的文件。
如果想让子进程单独处理新获取的连接,对于子进程来说,不关心之前创建的其他连接,只关心子进程本身继承下来的文件描述符,比如4号fd。对于曾经已经打开的文件建议关闭。如果不关,万一将来子进程误对3号fd进行操作。父进程对新获取的连接进行关闭,父进程不需要这个文件,新连接已经交给了子进程,让子进程进行处理。
对于不使用的文件描述符,需要及时关闭,避免文件描述符泄漏。
因此,在多进程版本中,父进程在完成任务后,继续返回到获取连接那里,子进程完成获取到的新连接的任务。这样服务器实现了多进程并发式获取连接。
子进程创建后未来会结束,一旦结束就会先进入僵尸状态,等待父进程进行wait
读取,一旦被读取,僵尸进程才会被释放掉,如果不读取,子进程将会一直处于僵尸状态。当前父进程采用的是阻塞式wait
,当子进程被创建,子进程在处理任务时,父进程会一直阻塞等待,如果子进程一直不死,父进程就一直在阻塞等待。此时依旧是等子进程结束,父进程才开始工作,简单来说这不是并发,依然还是一次只能处理一个连接。
解决方案:
- 使用信号量,这是最推荐的做法,但是过于简单
signal(SIGCHLD,SIG_IGN)
- 在子进程中再
fork
一次,让子进程直接退出,父进程的wait
可以立即返回,僵尸状态就可以立即被处理。此时孙子进程的父进程退出了,变成了孤儿进程,孤儿进程被系统领养,退出后,OS会对他进程回收,爷爷进程就不用关心了。
完整代码
#pragma once
#include<iostream>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/wait.h>
#include"Log.hpp"
#include"InetAddr.hpp"
using namespace log_ns;
enum
{
SOCKET_ERROR=1,
BAND_ERROR,
LISTEN_ERROR
};
const static int gport=8888;
const static int gsock=-1;
const static int gblcklog=8;
class TcpServer
{
public:
TcpServer(uint16_t port=gport)
:_port(port)
,_listensockfd(gsock)
,_isrunning(false)
{}
void InitServer()
{
//创建
_listensockfd=::socket(AF_INET,SOCK_STREAM,0);
if(_listensockfd<0)
{
exit(SOCKET_ERROR);
LOG(FATAL,"socket create error\n");
}
LOG(INFO,"socket create sussess,listensockfd: %d\n",_listensockfd); //3
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;
//绑定:sockfd 和 socket addr
if(::bind(_listensockfd,(struct sockaddr*)&local,sizeof(local))<0)
{
LOG(FATAL,"bind error\n");
exit(BAND_ERROR);
}
LOG(INFO,"bind success\n");
// 设置监听状态 listen
if(::listen(_listensockfd,gblcklog)<0)
{
LOG(FATAL,"listen error\n");
exit(LISTEN_ERROR);
}
LOG(INFO,"listen success\n");
}
void Loop()
{
//signal(SIGCHLD,SIG_IGN);
_isrunning=true;
while(_isrunning)
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
// 获取新连接
int sockfd=::accept(_listensockfd,(struct sockaddr*)&client,&len);
if(sockfd<0)
{
//获取连接失败
LOG(WARING,"accept error\n");
continue;
}
InetAddr addr(client);
LOG(INFO,"get a new link,client info: %s, sockfd is: %d\n",addr.AddrStr().c_str(),sockfd);
//提供服务--version 0
//Service(sockfd,addr);
//提供服务--version 1(多进程版)
pid_t id=fork();
if(id==0)
{
//child
::close(_listensockfd);
if(fork()>0) exit(0);
Service(sockfd,addr);
exit(0);
}
//fathre
::close(sockfd);
int n=waitpid(id,nullptr,0);
if(n>0)
{
LOG(INFO,"wait child success\n");
}
}
_isrunning=false;
}
//提供服务--version 0
void Service(int sockfd,InetAddr addr)
{
//长服务
while(true)
{
char inbuffer[1024];
ssize_t n=::read(sockfd,inbuffer,sizeof(inbuffer)-1);
if(n>0)
{
std::string echo_string="[sever echo] # ";
echo_string+=inbuffer;
write(sockfd,echo_string.c_str(),echo_string.size());
}
else if(n==0)
{
LOG(INFO,"client %s quit\n",addr.AddrStr().c_str());
break;
}
else
{
LOG(ERROR,"read error: %s\n",addr.AddrStr().c_str());
break;
}
}
::close(sockfd);
}
~TcpServer()
{}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning;
};
为什么两次都是4号fd?
父进程获取第一次连接后,将4号fd关闭了,此时4号fd是空的,第二次进程连接的时候,依然用这个4号fd。
服务端提供信息version–2(多线程版)
需要让新线程分离,如果在创建一个新线程后,让主线程等待,这不是并发式。
这里需要让新线程分离,这样新线程执行完毕后,会直接被回收。
在多线程中,所有的文件描述符表都是共享的,因此不能对不需要的文件描述符进行关闭。
#pragma once
#include<iostream>
#include<cstring>
#