本篇博客先看后面的代码,再回来看上面这些内容。
.hpp文件,基本调用
服务器基本框架
接下来用tcp的方式创建套接字,我们用SOCK_STREAM
因为TCP是面向连接的,所以当我们通信的时候需要建立连接,
将套接字状态转为监听状态,第一个参数套接字,第二个参数全连接队列长度(后续会详解这个参数)。
成功返回0,否则返回-1
如何理解监听状态?
由于TCP通信是需要连接的,即如果对方发过来连接我们要即使进行接收,listen是监听来自客户端的tcp socket的连接请求,即listen是看看客户端有没有发送连接请求过来。
我们在tcp_server.hpp里面完成创建tcp时初始化服务器的三大步:1.创建套接2.绑定3.listen
当我们启动服务器之后,用netstat -antp 可查看tcp相关的一些服务,加l之后,只会显示监听状态的服务,下图中listen表示当前服务器处于监听状态,即此时服务器随时等待别人的请求。
当服务器启动之后要先获取连接,有人连服务器时,服务器才能获取连接,若没人连服务器,服务器就处于阻塞状态,这里我们要这个接口。
获取新连接,第一个参数是套接字,第二个参数是输出型参数,第三个参数是输入输出型参数。
accept成功之后返回一个套接字,失败返回-1
后俩个参数和recvfrom后俩个参数含义一模一样,代表的是客户端的ip和客户端的端口号
accept的返回值sock和我们自己的_sock有何区别?
_sock的核心任务只有一个:获取新连接。而未来真正进行网络服务的是sock。_sock只是帮助底层的accept把底层的连接获取上来。这个_sock我们更倾向于称作listensock,也俗称监听套接字。上图里得sock一般成为servicesock,服务套接字。
_sock是通过accept获取新的连接,sock负责网络服务。
当服务器创建连接成功后,服务器需要读取对方发送过来的信息,注意:这里不能用recvfrom这个专门用于udp报文,是面向数据报的,我们用下面这个接口。这个就是我们之前在OS经常用到的接口。
我们可用telnet+IP+端口号去连接我们的服务器
之后按ctrl+],就可以发消息,此时发送的消息会被服务器收到
telnet的发送完信息之后,会立马读取对方发过来的信息,我们这里让服务器收到信息后,把信息又返回给了对方,因此这里左边消息出现了俩次。
telnet退出的时候输入按ctrl+],之后输入quit.此时telnet会自动退出,根据我们所写的程序,服务器也会自动退出。
当我们创建俩个客户端,给服务器发信息时,第二个客户端发不出去。
当第一个客户端退出之后,第二个客户端发送的消息会立刻被服务器接收。
这种情况又如何解决呢?这里我们写的是单进程,而且service里面是死循环,死循环一直在进行读取,若不退出就一直在读写。我们可以写一个多进程版
创建子进程,让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?
能,创建子进程时,子进程会继承父进程的文件描述符表,所以会和父进程看到同一个文件。
这里让父进程创建连接,子进程去给客户端提供服务。
但是当子进程退出之后会进入僵尸状态,父进程需要回收子进程,而父进程在等待子进程时是阻塞时等待,子进程不退出,父进程wait就不会返回,但由于这里有多个客户端,就不能使用waitpid,我们需要一种不阻塞式的等待子进程。如果这里子进程较多,用waitpid非阻塞等待比较麻烦。
子进程退出时会向父进程发送SIGCHILD信号,如果主动忽略SIGCHILE信号,子进程退出的时候会自动释放自己的僵尸状态。
父进程至少打开了这俩个套接字,即俩个文件描述符。
创建了子进程,子进程会继承父进程打开的文件与文件fd。
这里子进程是来进行提供服务的,不需要知道listensock,只需要servicesock,因此要让子进程关闭掉不需要的listensock,对于父进程只需要关闭自己不需要的servicesock套接字。父进程永远只保留listensock即可。
- #pragma once
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #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<
":"<"# "< - }
- 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
-
- 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
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
- #include
- #include
- #include
- #include
-
- // 日志是有日志级别的
- #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);
- }