本篇博客先看后面的代码,再回来看上面这些内容。
.hpp文件,基本调用
服务器基本框架
接下来用tcp的方式创建套接字,我们用SOCK_STREAM
因为TCP是面向连接的,所以当我们通信的时候需要建立连接,
listen
将套接字状态转为监听状态,第一个参数套接字,第二个参数全连接队列长度(后续会详解这个参数)。
成功返回0,否则返回-1
如何理解监听状态?
由于TCP通信是需要连接的,即如果对方发过来连接我们要即使进行接收,listen是监听来自客户端的tcp socket的连接请求,即listen是看看客户端有没有发送连接请求过来。
我们在tcp_server.hpp里面完成创建tcp时初始化服务器的三大步:1.创建套接2.绑定3.listen
当我们启动服务器之后,用netstat -antp 可查看tcp相关的一些服务,加l之后,只会显示监听状态的服务,下图中listen表示当前服务器处于监听状态,即此时服务器随时等待别人的请求。
当服务器启动之后要先获取连接,有人连服务器时,服务器才能获取连接,若没人连服务器,服务器就处于阻塞状态,这里我们要这个接口。
accept
获取新连接,第一个参数是套接字,第二个参数是输出型参数,第三个参数是输入输出型参数。
accept成功之后返回一个套接字,失败返回-1
后俩个参数和recvfrom后俩个参数含义一模一样,代表的是客户端的ip和客户端的端口号
accept的返回值sock和我们自己的_sock有何区别?
_sock的核心任务只有一个:获取新连接。而未来真正进行网络服务的是sock。_sock只是帮助底层的accept把底层的连接获取上来。这个_sock我们更倾向于称作listensock,也俗称监听套接字。上图里得sock一般成为servicesock,服务套接字。
_sock是通过accept获取新的连接,sock负责网络服务。
read|write
当服务器创建连接成功后,服务器需要读取对方发送过来的信息,注意:这里不能用recvfrom这个专门用于udp报文,是面向数据报的,我们用下面这个接口。这个就是我们之前在OS经常用到的接口。
单进程循环版
我们可用telnet+IP+端口号去连接我们的服务器
之后按ctrl+],就可以发消息,此时发送的消息会被服务器收到
telnet的发送完信息之后,会立马读取对方发过来的信息,我们这里让服务器收到信息后,把信息又返回给了对方,因此这里左边消息出现了俩次。
telnet退出的时候输入按ctrl+],之后输入quit.此时telnet会自动退出,根据我们所写的程序,服务器也会自动退出。
当我们创建俩个客户端,给服务器发信息时,第二个客户端发不出去。
当第一个客户端退出之后,第二个客户端发送的消息会立刻被服务器接收。
这种情况又如何解决呢?这里我们写的是单进程,而且service里面是死循环,死循环一直在进行读取,若不退出就一直在读写。我们可以写一个多进程版
创建子进程,让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?
能,创建子进程时,子进程会继承父进程的文件描述符表,所以会和父进程看到同一个文件。
这里让父进程创建连接,子进程去给客户端提供服务。
但是当子进程退出之后会进入僵尸状态,父进程需要回收子进程,而父进程在等待子进程时是阻塞时等待,子进程不退出,父进程wait就不会返回,但由于这里有多个客户端,就不能使用waitpid,我们需要一种不阻塞式的等待子进程。如果这里子进程较多,用waitpid非阻塞等待比较麻烦。
子进程退出时会向父进程发送SIGCHILD信号,如果主动忽略SIGCHILE信号,子进程退出的时候会自动释放自己的僵尸状态。
父进程至少打开了这俩个套接字,即俩个文件描述符。
创建了子进程,子进程会继承父进程打开的文件与文件fd。
这里子进程是来进行提供服务的,不需要知道listensock,只需要servicesock,因此要让子进程关闭掉不需要的listensock,对于父进程只需要关闭自己不需要的servicesock套接字。父进程永远只保留listensock即可。
tcp_server.hpp
#pragma once
#include<iostream>
#include<cassert>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string>
#include<signal.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<memory>
#include<cerrno>
#include<cstring>
#include"log.hpp"
static void service(int sock,const std::string&clientip,const uint16_t&clientport)
{
//收到对方的消息后,进行一些转换再返回给对方
char buffer[1024];
while(true)
{
//read和write可以直接被使用!
ssize_t s=read(sock,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
//将发过来的数据当作字符串
std::cout<<clientip<<":"<<clientport<<"# "<<buffer<<std::endl;
}
else if(s==0)
//返回值为0,代表对方关闭了连接,啥都没读到
{
logMessage(NORMAL,"%s:%d shutdown,me too!",clientip.c_str(),clientport);
break;
}
else
{
logMessage(ERROR,"read socket error,%d:%s",errno,strerror(errno));
break;
}
//走到这就读取成功了,我们接下来向套接字当中写入
write(sock,buffer,strlen(buffer));
}
}
class TcpServer
{
private:
const static int gbacklog=20;//这是listen的第二个参数
//一般不能太大也不能太小,后面会解释,这时listen的第二个参数
public:
TcpServer(uint16_t port,std::string ip=""):listensock(-1),_port(port),_ip(ip)
{}
//初始化服务器
void initServer()
{
listensock=socket(AF_INET,SOCK_STREAM,0);//返回值照样是文件描述符
//参数含义第一个:网络通信 第二个:流式通信
if(listensock<0)//创建套接字失败
{
logMessage(FATAL,"create socket error,%d:%s",errno,strerror(errno));//打印报错信息
exit(2);
}
logMessage(NORMAL,"create socket success,sock:%d",listensock);//打印套接字,它的文件描述符是3
//到这里创建套接字成功,接下来进行bind
//bind目的是让IP和端口进行绑定
//我们需要套接字,和sockaddr(这个里面包含家族等名称)
//绑定——文件和网络
struct sockaddr_in local;
memset(&local,0,sizeof local);//初始化local
local.sin_family=AF_INET;
local.sin_port=htons(_port);//端口号,端口是主机序列,我们需要主机转网络
local.sin_addr.s_addr=_ip.empty()?INADDR_ANY:inet_addr(_ip.c_str());
//IP地址,由于我们构造的时候是IP是个空的字符串
//所以我们可以绑定任意IP
//我们一般推荐绑定0号地址或特殊IP
//填充的时候IP是空的,就用INADDR_ANY否则用inet_addr
if(bind(listensock,(struct sockaddr*)&local,sizeof local)<0)
{
//走到这就绑定失败了,我们打印错误信息
logMessage(FATAL,"bind error,%d:%s",errno,strerror(errno));
exit(3);
}
//因为TCP是面向连接的,当我们正式通信的时候需要先建立连接。
//listen是在等待对方发过来连接,我们直接接收
if(listen(listensock,gbacklog)<0)
{
logMessage(FATAL,"listen error,%d:%s",errno,strerror(errno));
exit(4);
}
logMessage(NORMAL,"init server success");
}
//启动服务器
void start()
{
signal(SIGCHLD,SIG_IGN);//子进程自动释放自己的僵尸状态,用信号可以避免父进程阻塞等待子进程
//子进程退出,会向父进程发SIGCHLD信号,对SIGCHLD信号主动忽略,子进程退出的时候,会自动释放自己的僵尸状态
//服务器一旦启动就要周而复始的去运行
while(true)
{
//服务器启动之后先获取连接
//当有人连服务器的时候才能获取连接,若没人连就无法获取
//这里当没人连服务器的时候,我们让服务器一直进行阻塞。
struct sockaddr_in src;
socklen_t len=sizeof(src);
int servicesock=accept(listensock,(struct sockaddr*)&src,&len);
if(servicesock<0)
{
//获取连接失败
logMessage(ERROR,"accept error,%d:%s",errno,strerror(errno));
continue;
}
//获取连接成功了
uint16_t client_port=ntohs(src.sin_port);
//客户端端口号在src
//由于是网络发送过来得套接字信息
//所以要把信息进行网络转主机
std::string client_ip=inet_ntoa(src.sin_addr);
//我们需要将四字节网络序列的IP地址,转换成字符串风格的点分十进制的IP地址
//到这里我们拿到了IP和端口号
//谁连接服务器,服务器就拿到谁的信息
logMessage(NORMAL,"link success,servicesock:%d | %s : %d| \n",servicesock,client_ip.c_str(),client_port);
//这里servicesock是4,符合数组下标描述符的分配规则
//接下来进行通信服务
//verison1--单进程循环版——智能一次处理一个客户端,处理完一个客户端之后,才能处理下一个客户端
//service(servicesock,client_ip,client_port);
//version——2.1多进程版
//创建子进程,让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?
pid_t id=fork();
assert(id!=-1);
if(id==0)
{
//子进程
close(listensock);
//子进程只是来进行提供服务的,我们关闭它不需要的套接字,同理父进程也关闭自己不需要的套接字
service(servicesock,client_ip,client_port);//子进程开始提供服务
exit(0);//子进程退出进入僵尸状态
}
//父进程要回收子进程
close(servicesock);
}
}
~TcpServer(){}
private:
uint16_t _port;//端口号
std::string _ip;//ip地址
int listensock;
};
tcp_server.cc
#include "tcp_server.hpp"
#include <memory>
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
// ./tcp_server port
int main(int argc, char *argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<TcpServer> svr(new TcpServer(port));
svr->initServer();
svr->start();
return 0;
}
makefile
.PHONY:all
all:tcp_client tcp_server
tcp_client:tcp_client.cc
g++ -o $@ $^ -std=c++11 #-lpthread
tcp_server:tcp_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f tcp_client tcp_server
log.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./threadpool.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
if(level== DEBUG) return;
#endif
// va_list ap;
// va_start(ap, format);
// while()
// int x = va_arg(ap, int);
// va_end(ap); //ap=nullptr
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// vprintf(format, args);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
// FILE *fp = fopen(LOGFILE, "a");
printf("%s%s\n", stdBuffer, logBuffer);
// fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
// fclose(fp);
}