前引
鉴于笔者走的路线是 Linux后端开发 而且现在处于正在起步的阶段 而且发现网上对于 linux后端开发的项目 真的如何从零开始起步的文章太少了 所以笔者打算就从这篇开始 写一个后端刚起步的小白 慢慢一步步成长学习的路吧
当然 这肯定是会写一个系列的 最后的完结目标 是实现一个 底层实现思路类似于陈硕大佬的muduo网络库 基于基层网络库的框架之上 再实现出一个高性能静态http 并发的服务器 这个也会是作为之后实习的项目而做的
毕竟后端开发 一个高性能的WebServer 感觉应该是必须要去选择做的项目 这里的话 就先推荐两本书籍吧 也是我由这两本书为基础 + muduo网络库源码 作为之后实现webserver 的参考
《Linux高性能服务器编程》- 游双 《Linux多线程服务器编程》(基于muduo C++网络库)- 陈硕
第一本基本上是读完了 第二本目前在笔者写的时候 读到了第八章 muduo网络库设计与实现 很明显 第一本书就更偏向于linux服务器最基础的介绍 作为入门书籍 我觉得最合适不过了 如果一刚开始就来啃这本《linux多线程服务器编程》的话 肯定会是相当痛苦的
正如笔者现在的状态一样 阅读这本书的时候 揣着太多太多的疑惑与不解 如同在冰面上行走一样 稍有不慎便会一下子摔在地上 因为感觉看的时候确实太多疑惑与不解了
所以正是因为这样 笔者才会来写下这篇博客 并由此作为起点来记录 到最后由一个网络库实现出一个高性能静态web服务器
(一) 万丈高楼平地起 步子得一步一步慢慢走
何以取这个标题 哈哈 因为笔者打算先暂时停止看第二本《Linux高性能服务器编程》 而是先去把第一本书中 最后的 具有比较高的耦合性 线程池 + one loop per thread 给实现出来 再继续去看那本书了
个人以为 作为初学者 直接上手这本书只会是心中徒有疑惑 原本我的计划是第一本书的源码放着 直接来看第二本书 网络库 不需要有前置的铺垫 直接来就好了 结果发现确实不行
所以这一篇标题取名为 万丈高楼平地起啊 先把一个有着雏形的webserver给写出来 捣鼓捣鼓出来了 整明白了 我们再说 之后建立网络库 套模板 去改良吧
顺带呢 再把第一本书里面的 HTTP 状态转移机给弄懂整出来吧 因为后面反正也用得到 说到状态转移 我就想到了之前 我做正则引擎的时候 最后用跳转表 前面的NFA DFA的状态转移 哈哈 所以这里看到这个词心里面就觉得倍感亲切啊 哈哈
废话也不多说了 我先去把源代码一行行给看懂了 然后凭借着自己的印象多少给捣鼓捣鼓出来再说吧
1、simple echo server 0.01
为什么笔者打算先从一个最简单的能够响应的echo 服务器开始下手呢 说来惭愧 看了看Aupe 和Unp 自己一行代码也没敲过 包括看完第一本推荐的书的时候也是一行代码也没敲过
尽管很多框架零零碎碎的晓得了吧 但是没有自己上手过的东西总是虚的 那不妨先从一个有基本功能的echo server 开始写起 然后支持并发啊 各种细节开始扣 然后最后再向http server 开始写 毕竟http server 呢 也只不过就是多了一个协议报头需要处理 还要一些其他的细节就要处理 高复用性的框架写出来了 那把服务器往里面套就完了
1、熟悉基本工具
所有的项目 东西都是一步一步积累起来的 本来想先写一下进程/线程池的 算了 先把一个最基本 没有协议 能够支持单行发送消息 最简单的发送消息的服务器写出来吧
其实刚开始尝试开头 我便感觉很多东西已经开始了 写一段这样简单的simple的代码 建议的话呢 别先去copy 每一行代码 每一个命令都先自己根据书里面讲的函数去写 到后面g++ 编译的时候 就可以发现自己哪里写错了 哪里有问题需要改
也是从这里开始 我也打算正式的 以后把vim 作为我在Linux上面敲代码正式的编辑器 而不是那种文本编辑器了 在我写完一个simple server时 我才真的明白 就强迫自己去用这些快捷键 用多了之后发现真的很方便很便捷 用完vim 后呢 对于客户端 也可以用用nc 这样的瑞士军刀 来充当客户端 用用这些实用的工具 不管之前很多东西用的多顺 去尝试用这些实用 熟悉了能够大幅度提高生产力的小工具 还是相当不错的 之后的话 如果服务器出错了 也可以尝试用tcpdump 去抓抓包 反正这里写的意义呢 就是仅仅在于希望各位笔者能够自己先开始用这些好用的tools 了
2、simple echo server 0.01 came out
这里就不多介绍了 也就从这里正式开始 一步步拓宽 一步步开始进阶的改善这个echo server了 最后再测试一下效果
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc,char* argv[])
{
if (argc <= 2)
{
printf( "Usage: %s ip_address portname\n", argv[0] );
return 0;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 1 );
struct sockaddr_in address;
memset( &address, 0, sizeof( address ) );
address.sin_family = AF_INET;
address.sin_port = htons( port );
inet_pton( AF_INET, ip, &address.sin_addr );
int ret = 0;
ret = bind( listenfd, (struct sockaddr*)( &address ),
sizeof( address ) );
assert( ret != -1 );
ret = listen( listenfd, 5 );
assert( ret != -1 );
struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );
int sockfd = accept( listenfd, (struct sockaddr*)( &address ), &client_addrlength );
char buf_size[1024] = {0};
int recv_size = 0;
recv_size = recv( sockfd, buf_size, sizeof( buf_size ) , 0);
int send_size = 0;
send_size = send( sockfd, buf_size , recv_size , 0 );
close( sockfd );
close( listenfd );
return 0;
}
3、simple server 0.0.1 test
用一下实用的nc
服务器端 先打开服务器端 然后输入ip地址 选择回流的本地地址 然后端口选择2022
客户端 由于懒得写 就用nc命令来充当客户端了 用了才发现确实好用 哈哈 有尝试才有收获 光说不做假把式
结果发现 输入完第一条消息 确实回弹了第一条消息 但是客户端没有关闭 服务器由于笔者写的只返回第一条消息 直接关闭服务器 所以后面
2、simple echo server 0.05
莫想要一步登天 一口气吃成个胖子 一步步熟悉好每个函数 每个作用 一步布把脚步放踏实点 自己走上去才觉得心安嘛 嘿嘿 这里的话 就要引入出 单线程echo server 服务器了 只不过现在可以并发啦 能够处理并发但不是多线程的话 是因为我们用了I/O复用 Epoll函数 这种最基本的模型的话 在第二本书里面介绍过 应该是单线程Reactor了 epoll触发呢 采用的边沿ET触发 而不是LT触发 这一版本较上面一版本的话 肯定还是需要去再扣一下echo server的细节
话也不多说 走起吧
1、simple echo server 0.05 came out
写了一会 代码也都自己分析了一遍 自己开了几个端口 其实也就是用了一下 epoll 原本我以为很快就写完了 结果还是花了一点时间 把写错的代码改回来 - - 下面直接放代码了
其实仔细想一下 就会很简单的发现下面代码有可能是有问题的 是因为有可能echo返回的字符串不完整 不是一整行 所以应该是需要前面加个 每一行的字节数 才能确定是否send了一个完整的行 既没有多也没有少
这部分就留到之后再完成吧
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#define MAX_EVENTS_NUMBER 5
int set_non_blocking( int fd )
{
int old_state = fcntl( fd, F_GETFL );
int new_state = old_state | O_NONBLOCK;
fcntl( fd, F_SETFL, new_state );
return old_state;
}
void addfd( int epollfd , int fd )
{
epoll_event event;
event.events = EPOLLIN | EPOLLET;
event.data.fd = fd;
epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
set_non_blocking( fd );
}
int main( int argc , char* argv[] )
{
if (argc <= 2)
{
printf( "Usage: %s ip_address portname\n", argv[0] );
return 0;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 1 );
struct sockaddr_in address;
memset( &address, 0, sizeof( address ) );
address.sin_family = AF_INET;
address.sin_port = htons( port );
inet_pton( AF_INET, ip, &address.sin_addr );
int ret = 0;
ret = bind( listenfd, (struct sockaddr*)( &address ),
sizeof( address ) );
assert( ret != -1 );
ret = listen( listenfd, 5 );
assert( ret != -1 );
epoll_event events[ MAX_EVENTS_NUMBER ];
int epollfd = epoll_create( 5 );
assert( epollfd != -1);
addfd( epollfd, listenfd );
while(1)
{
int number = epoll_wait( epollfd, events, MAX_EVENTS_NUMBER, -1 );
if( number < 0 )
{
printf( "epoll_wait failed\n" );
return -1;
}
for( int i = 0; i < number; ++i )
{
const auto& event = events[i];
const auto eventfd = event.data.fd;
if( eventfd == listenfd )
{
struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );
int sockfd = accept( listenfd, ( struct sockaddr* )( &address ),
&client_addrlength );
addfd( epollfd, sockfd );
}
else if( event.events & EPOLLIN )
{
char buf[1024] = {0};
while(1)
{
memset( buf, '\0', sizeof( buf ) );
int recv_size = recv( eventfd, buf, sizeof( buf ), 0 );
if( recv_size < 0 )
{
if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) )
{
break;
}
printf(" sockfd %d,recv msg failed\n", eventfd );
close( eventfd );
break;
}
else if( recv_size == 0)
{
close( eventfd );
break;
}
else
{
send( eventfd, buf, recv_size, 0 );
}
}
}
}
}
close( listenfd );
return 0;
}
2、simple server 0.0.5 test
这里就简单的放一张图吧 下面就是两个client 一起发送消息的截图 我设置的是10秒后自动断连 所以在10秒后回到了shell
这里算是简单的测试了一下 想要继续测试的大家可以自己去尝试啦~
结束语
这部分算是最基础最基础的一些api的使用了 当然 这也是我第一次在c++/c环境下用这些api 也是第一次用epoll 感觉使用起来还是非常的简单的 哈哈
第一次感觉到了那句话 入门简单 精通难 确实是这样的 但第一篇作为入门的目的已经做到了 第一篇中 我们还是最后尝试去用了 单线程Reactor的I/O复用
在这里呢 我打算第二篇就开始前往 线程池 和 陈硕介绍的 one loop per thread 方向走了 其实这个one loop per thread呢 就是在第一本书中介绍的 那个高效的 半同步/半异步I/O模型
下面还是贴一下图吧 遗憾的是什么呢 用了one loop per thread 模型的代码 是用的进程池 用了线程池的代码呢 是用的全局队列的方式 也就是每个线程在一个全局队列中争抢业务
但是作为第一篇 个人感觉还是开了一个不错的好头的 至少让人感觉到 初初摸到了门路 哈哈 那这一篇就先写到这里 我们下一篇见了
|