代码部分
http.h
#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<sys/stat.h>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/mman.h>
#include<stdarg.h>
#include<errno.h>
#include<sys/wait.h>
#include<sys/uio.h>
#include<map>
#include<../lock/locker.h>
#include<../CGImysql/sql_connection_pool.h>
#include<../timer/lst_time.h>
#include<../log/log.h>
class http_conn{
public: //定义一些常量值
//文件名、缓冲区大小
static const int FILENAME_LEN = 200;
static const int READ_BUFFER_SIZE = 2048;
static const int WRITE_BUFFER_SIZE = 1024;
//请求方法
enum METHOD {
GET = 0,
POST,
HEAD,
PUT,
DELETE,
TRACE,
OPTIONS,
CONNECT,
PATH
};
//主状态的三种状态
enum CHECK_STATE {
CHECK_STATE_REQUESTLINE = 0, //解析请求行
CHECK_STATE_HEADER , //解析头部信息
CHECK_STATE_CONTENT //解析正文 -- POST请求
};
//HTTP的响应码
enum HTTP_CODE {
NO_REQUEST, //请求不完整,需要继续读取客户端数据
GET_REQUEST, //获得了一个完整的客户端请求
BAD_REQUEST, //客户端请求有语法问题
NO_RESOURSE, //没有该资源
FORBIDENT_RESOURCE, //客户对资源没有足够的访问权限
FILE_REQUEST, //文件请求?
INTERANL_ERROR,//表示服务器内部错误
CLOSED_CONNECTION //客户端已关闭连接
};
//从状态机的三种状态
//从状态机每次从缓冲区读取一行信息,直至读取到 \r\n 表示读取到一行,
//同时将 \r\n 替换为 \0\0 便于主状态机读取该行,然后再将行起始标志定位到下一行的起始位置。
enum LINE_STATUS{
LINE_OK , //完整读取一行
LINE_OPEN, //读取的行不完整
LINE_BAD //读取的报文有误
};
private: //成员变量
int m_sockfd;
sockaddr_in m_address;
char m_read_buf[READ_BUFFER_SIZE];
int m_read_idx; //目前已经读取了多少字节的客户数据
int m_checked_idx; //目前已经分析完了多少字节的客户数据
int m_start_line; //行在buffer中的起始位置
char m_write_buf[WRITE_BUFFER_SIZE];
char m_write_idx;
CHECK_STATE m_check_state;
METHOD m_method;
char m_real_file[FILENAME_LEN]; //存放文件名
char *m_url; //客户端请求的资源路径
char *m_version;
char *m_host;
int m_content_length;
bool m_linger;
char *m_file_address;
struct stat m_file_state;
struct iovec m_iv[2]; //
int m_iv_count; //缓冲区个数
int cgi; //是否启用post
char *m_string; //存储请求头数据
int bytes_to_send;
int bytes_have_send;
char *doc_root; //doc_root 是资源文件根目录
map<string,string>m_users;
int m_TRIGMode;
int m_close_log;
char sql_user[100];
char sql_password[100];
char sql_name[100];
public:
http_conn(){}
~http_conn(){}
public:
void init(int sockfd, const scokaddr_in &addr, char*, int, int, string user, string password, string sqlname);
void close_conn(bool real_close = true);
void process();
bool read_once();
bool write();
sockaddr_in *get_address(){
return &m_address;
}
void initmysql_result(connection_pool *connPool);
int timer_flag;
int improv;
private:
void init();
HTTP_CODE process_read();
bool process_write(HTTP_CODE ret);
HTTP_CODE parse_request_line(char* text); //解析请求行
HTTP_CODE parse_headers(char* text); //解析请求头
HTTP_CODE parse_content(char* text); //判断http请求是否被完整读入
HTTP_CODE do_request();
char* get_line(){
return m_read_buf + m_start_line;
}
LINE_STATUS parse_line(); //从状态机
void unmap();
bool add_response(const char* format,...);
bool add_content(const char* content);
bool add_status_line(int status, const char*title);
bool add_headers(int content_length);
bool add_content_type();
bool add_content_lenth(int content_length);
bool add_linger();
bool add_blank_line();
public:
static int m_epollfd;
static int m_user_count;
MYSQL *mysql;
int m_state; //读为0,写为1
};
#endif
http.cpp
#include<mysql/mysql.h>
#include<fstream>
#include"http_conn.h"
//定义http响应的一些状态信息
const char *ok_200_title = "OK";
const char *error_400_title = "Bad Request";
const char *error_400_form = "Your request has bad syntax or is inherently impossible to staisfy.\n";
const char *error_403_title = "Forbidden";
const char *error_403_form = "You do not have permission to get file form this server.\n";
const char *error_404_title = "Not Found";
const char *error_404_form = "The requested file was not found on this server.\n";
const char *error_500_title = "Internal Error";
const char *error_500_form = "There was an unusual problem serving the request file.\n";
locker m_lock;
map<string, string> users;
//获取所有的用户名和对应的密码
void http_conn::initmysql_result(connection_pool * connPool){
//先从连接池中获取一个连接
MYSQL *mysql = NULL;
connectionRAII mysqlcon(&mysql,connection_pool);
//在user表中检索username,password 数据,浏览器端输入
if(mysql_query(mysql, "SELECT username, password FROM user")){
LOG_ERROR("SELECT error: %s\n", mysql_error(mysql));
}
//从表中检索完整的结果集
MYSQL_RES *result = mysql_store_result(mysql);
//返回结果集中的列数
int num_fields = mysql_num_fields(result);
//返回所有字段结构的数组
MYSQL_FIELD *fields = mysql_fetch_fields(result);
//从结果集中获取下一行,将对应的用户名和密码,存入map中
while(MYSQL_ROW row = mysql_fetch_row(result)){
string temp1(row[0]);
string temp2(row[1]);
users[temp1] = temp2;
}
}
//对文件描述符设置为非阻塞
int setnonblocking(int fd){
int old_option = fcntl(fd,F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd,F_GETFL,new_option);
return old_option; //为什么需要返回这个值?
}
//将内核事件表注册读事件、ET模式、选择开启EPOLLONESHOT
void addfd(int epollfd,int fd,bool one_shot, int TRIGMode){ // TRIGMode:选择触发模式 -- 条件触发 或者 边缘触发
epoll_event event;
event.data.fd = fd;
if(TRIGMode == 1) {
event.events = EPOLLIN | EPOLLET |EPOLLHUP; //EPOLLHUP:断开连接或者半关闭的状态
}else {
event.events = EPOLLIN |EPOLLHUP; //默认是条件触发模式
}
if(one_shot){
event.events |= EPOLLONESHOT; // 发生一次事件之后,相应的文件描述符不在接受事件通知,如果想要让它继续接受通知,需要重置
}
//注册事件文件描述符
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
//对文件描述符设置为非阻塞:如果不设置为非阻塞,没有文件数据读取时候,会阻塞,进程无法继续执行
setnonblocking(fd);
}
//从内核事件表中删除文件描述符
void removefd(int epollfd, int fd){
epoll_ctl(epollfd, EPOLL_CTL_DEL,fd);
close(fd); // 关闭连接
}
//将时间重置为EPOLLONESHOT
void modfd(int epollfd, int fd, int ev, int TRIGMode){ // TRIGMode:选择触发模式 -- 条件触发 或者 边缘触发
epoll_event event;
event.data.fd = fd;
if(TRIGMode == 1) {
event.events = ev | EPOLLIN | EPOLLONESHOT |EPOLLHUP; //EPOLLHUP:断开连接或者半关闭的状态
}else {
event.events = ev | EPOLLONESHOT |EPOLLHUP; //默认是条件触发模式
}
//注册事件文件描述符
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&event);
}
int http_conn::m_user_count = 0;
int http_conn::m_epollfd = -1;
//关闭连接,关闭一个连接,客户总量减一
void http_conn::close_conn(bool real_close){
if(real_close && (m_sockfd != -1)){
std::cout<<"close "<< m_sockfd <<std::endl;
removefd(m_epollfd,m_sockfd);
m_sockfd = -1;
m_user_count--;
}
}
//初始化连接,外部调用初始化套接字地址
void http_conn::init(int sockfd, const scokaddr_in &addr, char* root, int TRIGMode,
int closer_log, string user, string password, string sqlname) {
m_sockfd = sockfd;
m_address = addr;
//注册事件文件描述符
addfd(m_epollfd,sockfd,true,m_TRIGMode);
m_user_count++;
//当浏览器出现连接重置时,可能是网站根目录出错或http相应格式出错或者访问的文件内容完全为空
doc_root = root;
m_TRIGMode = TRIGMode;
m_close_log = closer_log;
strcpy(sql_user, user.c_str());
strcpy(sql_password, password.c_str());
strcpy(sql_name, sqlname.c_str());
init();
}
//初始化新接受的连接
//check_state默认为分析请求行状态
void http_conn::init()
{
mysql = NULL;
bytes_to_send = 0;
bytes_have_send = 0;
m_check_state = CHECK_STATE_REQUESTLINE;
m_linger = false;
m_method = GET;
m_url = 0;
m_version = 0;
m_content_length = 0;
m_host = 0;
m_start_line = 0;
m_checked_idx = 0;
m_read_idx = 0;
m_write_idx = 0;
cgi = 0;
m_state = 0;
timer_flag = 0;
improv = 0;
memset(m_read_buf, '\0', READ_BUFFER_SIZE);
memset(m_write_buf, '\0', WRITE_BUFFER_SIZE);
memset(m_real_file, '\0', FILENAME_LEN);
}
//从状态机,用于分析出一行内容
//返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN
http_conn::LINE_STATUS http_conn::parse_line(){
char temp;
/*m_checked_id 初始值为0*/
for(; m_checked_idx < m_read_idx; ++m_checked_idx) {
temp = m_read_buf[m_checked_idx];
if(temp == '\r') { //如果当前是回车符,则可能读取到了一个完整行
if((m_checked_idx + 1) == m_read_idx){ //当前是读到的最后一个字符,证明客户端还没发送完当前行
return LINE_OPEN;
}else if(m_read_buf[m_checked_idx + 1] == '\n'){ //每一行都是以[回车符][换行符]结尾的
m_read_buf[m_checked_idx++] = '\0'; //替换掉[回车符][换行符]
m_read_buf[m_checked_idx++] = '\0';
return LINE_OK;
}
return LINE_BAD;
}else if(temp = '\n'){//如果当前是换行符,则可能读取到了一个完整行
if(m_checked_idx > 1 && m_read_buf[m_checked_idx -1] == '\r'){//判断是不是[回车符][换行符]
m_read_buf[m_checked_idx-1] = '\0';
m_read_buf[m_checked_idx++] = '\0';
return LINE_OK; //完整行
}
return LINE_BAD;
}
}
return LINE_OPEN; /*如果一直没有遇到[回车符],证明当前读到的客户端数据并不完整,需要继续读取*/
}
//循环读取客户数据,直到无数据可读,或对方关闭连接
//非阻塞ET工作模式下,需要一次性将数据读完
bool http_conn::read_once(){
if(m_read_idx >= READ_BUFFER_SIZE){
return false;
}
int bytes_read = 0;
//LT读取数据
if(m_TRIGMode == 0) {
bytes_read = recv(m_sockfd,m_read_buf+m_read_idx,READ_BUFFER_SIZE - m_read_idx,0);
m_read_idx += bytes_read;
if(bytes_read <= 0){
return false;
}
return true;
}else { //ET读取数据
while(true){
bytes_read = recv(m_sockfd,m_read_buf+m_read_idx,READ_BUFFER_SIZE - m_read_idx,0);
if(bytes_read == -1){ //此时缓冲区中已没有数据可读啦
if(erron == EAGAIN || erron == EWOULDBLOCK){
//errno: 11 Resource temporarily unavailable
//#define EAGAIN 11 /* Try again */
break;
}
return false;
}else if(bytes_read == 0){ //当recv返回值等于0时,表示此时connect已经关闭,没有接收到数据。
return false;
}
m_read_idx += bytes_read;
}
return true;
}
}
//解析http请求行,获得请求方法,目标url 及 http 版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char*text){
//先判断一下是否合法:
m_url = strpbrk(text,"\t"); //该函数返回 str1 中第一个匹配字符串 str2 中字符的字符数,如果未找到字符则返回 NULL。
if(m_url == NULL) {
return BAD_REQUEST;
}
*m_url++ = '\0';
char *method = text;
//C语言中判断字符串是否相等的函数,忽略大小写。s1和s2中的所有字母字符在比较之前都转换为小写。
//该strcasecmp()函数对空终止字符串进行操作。函数的字符串参数应包含一个(’\0’)标记字符串结尾的空字符。
if(strcasecmp(method,"GET") == 0){
m_method = GET;
}else if(strcasecmp(method,"POST") == 0){
m_method = POST;
cgi = 1;
}else {
return BAD_REQUEST;
}
//计算字符串str中连续有几个字符都属于字符串accept
//size_t strspn(const char *str, const char * accept);
m_url += strspn(m_url, " \t");
m_version = strpbrk(m_url, " \t");
if (!m_version) {
return BAD_REQUEST;
}
*m_version++ = '\0';
m_version += strspn(m_version, " \t");
if (strcasecmp(m_version, "HTTP/1.1") != 0)
return BAD_REQUEST;
if (strncasecmp(m_url, "http://", 7) == 0)
{
m_url += 7;
m_url = strchr(m_url, '/');
}
if (strncasecmp(m_url, "https://", 8) == 0)
{
m_url += 8;
m_url = strchr(m_url, '/');
}
if (!m_url || m_url[0] != '/') {
return BAD_REQUEST;
}
//当url为/时,显示判断界面
if (strlen(m_url) == 1) {
strcat(m_url, "judge.html");
}
m_check_state = CHECK_STATE_HEADER; /*HTTP请求行处理完毕,状态转移到对请求头的分析*/
return NO_REQUEST; /*当前只是分析到请求行,请求不完整,需要继续从客户端读取数据*/
}
//解析http请求的一个头部信息 -- 关于浏览器的附加信息
http_conn::HTTP_CODE http_conn::parse_headers(char*text) {
//遇到空行,说明可能读取到了一个正确完整的HTTP请求头,主状态机转移到分析请求体的状态(GET方法没有请求体)
if(text[0] == '\0'){
if(m_content_length != 0) { //当前是POST方法,还有请求体,所以请求报文还没有分析完毕
m_check_state = CHECK_STATE_CONTENT;
return NO_REQUEST;
}
return GET_REQUEST; //当前是GET方法,没有请求体了,证明请求报文已经分析完毕
} else if(strncasecmp(text,"Connection:", 11) == 0){ //处理 Connection头部字段
text += 11;
text += strspn(text,"\t");
if(strncasecmp(text,"keep-alive") == 0){
m_linger = true;
}
} else if(strncasecmp(text,"Content-length:",15) == 0) { //处理 Content-length头部字段
text += 15;
text += strspn(text, "\t");
m_content_length = atol(text); //请求体的长度
} else if(strncasecmp(text,"Host:",5) == 0) { //处理 Host头部字段
text += 5;
text += strspn(text, "\t");
m_host = text;
}else { //其他头部字段都不进行处理
LOG_INFO("oop! unknow header: %s", text);
}
return NO_REQUEST; //没到空行,所以还没读到完整的请求报文
}
//判断http请求是否被完整读入
http_conn::HTTP_CODE http_conn::parse_content(char *text){
if(m_read_idx >= (m_content_length + m_checked_idx)){ //读取到的数据 >= 将要分析的数据 + 已经分析的数据
text[m_content_length] = '\0';
//POST请求中最后为输入的用户名和密码
m_string = text;
return GET_REQUEST;
}
return NO_REQUEST;
}
//主状态机:
http_conn::HTTP_CODE http_conn::process_read(){
LINE_STATUS line_status = LINE_OK;
HTTP_CODE ret = NO_REQUEST;
char *text = 0;
//开始分析请求体的数据
while((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || (line_status == parse_line() == LINE_OK)){
text = get_line(); //从缓冲区中读取一行数据
m_start_line = m_checked_idx;
LOG_INFO("%s",text);
switch(m_check_state) {
case CHECK_STATE_REQUESTLINE: { //状态1:分析请求行
ret = parse_request_line(text);
if(ret == BAD_REQUEST){
return BAD_REQUEST;
}
break;
}
case CHECK_STATE_HEADER: { //状态2:分析请求头
ret = parse_headers(text);
if(ret == BAD_REQUEST){
return BAD_REQUEST;
} else if(ret == GET_REQUEST) { //遇到空行,而且,m_content_length == 0,也就是没有请求体
return do_request();
}
break;
}
case CHECK_STATE_CONTENT: { //状态3:分析请求体
ret = parse_content(text);
if(ret == GET_REQUEST){
return do_request();
}
line_status = LINE_OPEN; //读取的请求报文信息不完整,需要继续从缓冲区中读取;
break;
}
default:
return INTERANL_ERROR;
}
}
return NO_REQUEST; //缓冲区接收到的请求报文也是不完整的,需要继续接收
}
http_conn::HTTP_CODE http_conn::do_request() {
strcpy(m_real_file, doc_root); /*doc_root 是资源文件根目录*/
int len = strlen(doc_root);
//printf("m_url:%s\n", m_url);
const char *p = strrchr(m_url, '/'); /*m_url: 客户端请求的资源路径*/
//处理cgi--POST请求
if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3'))
{
//根据标志判断是登录检测还是注册检测
char flag = m_url[1];
char *m_url_real = (char *)malloc(sizeof(char) * 200); //目标资源相对路径
strcpy(m_url_real, "/");
strcat(m_url_real, m_url + 2); //把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。
strncpy(m_real_file + len, m_url_real, FILENAME_LEN - len - 1);
free(m_url_real);
//m_real_file里面存放着 doc_root + m_url_real
//将用户名和密码提取出来
//user=123&passwd=123
char name[100], password[100];
int i;
for (i = 5; m_string[i] != '&'; ++i)
name[i - 5] = m_string[i];
name[i - 5] = '\0';
int j = 0;
for (i = i + 10; m_string[i] != '\0'; ++i, ++j)
password[j] = m_string[i];
password[j] = '\0';
if (*(p + 1) == '3')
{
//如果是注册,先检测数据库中是否有重名的
//没有重名的,进行增加数据
char *sql_insert = (char *)malloc(sizeof(char) * 200);
strcpy(sql_insert, "INSERT INTO user(username, passwd) VALUES(");
strcat(sql_insert, "'");
strcat(sql_insert, name);
strcat(sql_insert, "', '");
strcat(sql_insert, password);
strcat(sql_insert, "')");
//如果没有重名,注册
if (users.find(name) == users.end())
{
m_lock.lock(); //往数据库里面添加数据
int res = mysql_query(mysql, sql_insert);
users.insert(pair<string, string>(name, password));
m_lock.unlock();
if (!res)//注册成功-跳转到登录页面
strcpy(m_url, "/log.html");
else //注册失败,跳转到注册错误页面
strcpy(m_url, "/registerError.html");
}
else //有重名,注册失败
strcpy(m_url, "/registerError.html");
}
//如果是登录,直接判断
//若浏览器端输入的用户名和密码在表中可以查找到,返回1,否则返回0
else if (*(p + 1) == '2')
{
if (users.find(name) != users.end() && users[name] == password)
strcpy(m_url, "/welcome.html");
else
strcpy(m_url, "/logError.html");
}
}
//如果是GET请求:
if (*(p + 1) == '0') //注册页面
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/register.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
else if (*(p + 1) == '1') //登录页面
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/log.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
else if (*(p + 1) == '5') //申请图片资源
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/picture.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
else if (*(p + 1) == '6') //申请视频资源
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/video.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
else if (*(p + 1) == '7') //粉丝
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/fans.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
else
strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);
//获取文件信息
if (stat(m_real_file, &m_file_stat) < 0)
return NO_RESOURCE;
/*
st_mode 主要包含了 3 部分信息:
15-12 位保存文件类型
11-9 位保存执行文件时设置的信息
8-0 位保存文件访问权限
*/
if (!(m_file_stat.st_mode & S_IROTH))
return FORBIDENT_RESOURCE;
if (S_ISDIR(m_file_stat.st_mode))
return BAD_REQUEST;
int fd = open(m_real_file, O_RDONLY);
//将文件映射到内存中,并且返回实际分配的内存的起始地址
m_file_address = (char *)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);
return FILE_REQUEST;
}
void http_conn::unmap()
{
if (m_file_address)
{
//解除文件的映射,相当于从内存中删除该文件
munmap(m_file_address, m_file_stat.st_size);
m_file_address = 0;
}
}
bool http_conn::write()
{
int temp = 0;
//准备下一次的响应:
if (bytes_to_send == 0)
{
//将事件重置为EPOLLONESHOT
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
init();
return true;
}
// bytes_to_send != 0, 有数据需要发送给客户端
while (1)
{
//将数据写入客户端套接字,如果成功,返回写入的数据量
temp = writev(m_sockfd, m_iv, m_iv_count);
if (temp < 0)
{
if (errno == EAGAIN) //传输数据失败,重新设置事件
{
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
return true;
}
unmap(); //接触资源的内存映射
return false;
}
//成功发送一次数据
bytes_have_send += temp;
bytes_to_send -= temp;
//如果剩下需要写入的数据超过第一个缓冲区的接收的最大长度
if (bytes_have_send >= m_iv[0].iov_len)
{
m_iv[0].iov_len = 0; //关闭第一个缓冲区
m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx); //使用第二个缓冲区
m_iv[1].iov_len = bytes_to_send; //将缓冲区的大小设置为剩下需要写入的数据量
}
else //否则,继续使用第一个缓冲区
{
m_iv[0].iov_base = m_write_buf + bytes_have_send;
m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send; //将缓冲区的大小设置为剩下需要写入的数据量
}
//数据传输完成
if (bytes_to_send <= 0)
{
unmap(); //解除内存映射
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
if (m_linger) //如果是可持续连接,就继续保持和客户端的连接状态
{
init(); //重新初始化,准备下一次的交互
return true;
}
else
{
return false;
}
}
}
}
bool http_conn::add_response(const char *format, ...)
{
if (m_write_idx >= WRITE_BUFFER_SIZE)
return false;
va_list arg_list; // 可变参数列表
va_start(arg_list, format); //把format 之后的参数都作为可变参数
//向一个字符串缓冲区打印格式化字符串,指定目标位置、大小、格式、内容,
//成功打印到sbuf中的字符的个数,不包括末尾追加的\0。如果格式化解析失败,则返回负数。
int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
//限定最多打印到缓冲区sbuf的字符的个数为WRITE_BUFFER_SIZE - 1 - m_write_id-1个,如果超过,那就是出错了
if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx))
{
va_end(arg_list);//将arg_ptr指针置0
return false;
}
m_write_idx += len;
va_end(arg_list);//将arg_ptr指针置0
LOG_INFO("request:%s", m_write_buf);
return true;
}
//响应报文的第一部分 -- 响应行
bool http_conn::add_status_line(int status, const char *title)
{
return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
}
//响应报文的第二部分 -- 响应头
bool http_conn::add_headers(int content_len)
{
return add_content_length(content_len) && add_linger() &&
add_blank_line();
}
bool http_conn::add_content_length(int content_len)
{
return add_response("Content-Length:%d\r\n", content_len);
}
bool http_conn::add_content_type()
{
return add_response("Content-Type:%s\r\n", "text/html");
}
bool http_conn::add_linger()
{
return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close");
}
bool http_conn::add_blank_line()
{
return add_response("%s", "\r\n");
}
//响应报文的第三部分 -- 响应体
bool http_conn::add_content(const char *content)
{
return add_response("%s", content);
}
bool http_conn::process_write(HTTP_CODE ret)
{
switch (ret)
{
case INTERNAL_ERROR:
{
add_status_line(500, error_500_title);
add_headers(strlen(error_500_form));
if (!add_content(error_500_form))
return false;
break;
}
case BAD_REQUEST:
{
add_status_line(404, error_404_title);
add_headers(strlen(error_404_form));
if (!add_content(error_404_form))
return false;
break;
}
case FORBIDENT_RESOURCE:
{
add_status_line(403, error_403_title);
add_headers(strlen(error_403_form));
if (!add_content(error_403_form))
return false;
break;
}
case FILE_REQUEST: //正常的资源申请
{
add_status_line(200, ok_200_title);
if (m_file_stat.st_size != 0) // 如果响应体有数据
{
add_headers(m_file_stat.st_size);
//设置缓冲区结构体:
m_iv[0].iov_base = m_write_buf; //客户端缓冲区
m_iv[0].iov_len = m_write_idx;
m_iv[1].iov_base = m_file_address; //目标文件
m_iv[1].iov_len = m_file_stat.st_size;
m_iv_count = 2;
bytes_to_send = m_write_idx + m_file_stat.st_size;
return true;
}
else //响应体没有数据
{
const char *ok_string = "<html><body></body></html>";
add_headers(strlen(ok_string));
if (!add_content(ok_string))
return false;
}
}
default:
return false;
}
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
m_iv_count = 1;
bytes_to_send = m_write_idx;
return true;
}
void http_conn::process()
{
HTTP_CODE read_ret = process_read();
if (read_ret == NO_REQUEST)
{
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
return;
}
bool write_ret = process_write(read_ret);
if (!write_ret)
{
close_conn();
}
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
}
学习笔记
http连接处理类
根据状态转移,通过主从状态机封装了http连接类。其中,主状态机在内部调用从状态机,从状态机将处理状态和数据传给主状态机
- 客户端发出http连接请求
- 从状态机读取数据,更新自身状态和接收数据,传给主状态机
- 主状态机根据从状态机状态,更新自身状态,决定响应请求还是继续读取
实现思路
HTTP协议的基本了解
- HTTP请求报文格式
请求方法[空格]URL[空格]协议版本[回车符][换行符] ==》请求行
头部字段[:]值[回车符][换行符] ==》请求头
…
头部字段[:]值[回车符][换行符]
[回车符][换行符] ==》空行
请求数据 ==》请求体
注意:GET / HTTP/1.1 Host: www.baidu.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
- POST提交方式的数据在请求体中,也就是,GET方法没有请求体
- 回车符\r , 换行符 \n
- HTTP响应报文格式
协议版本[空格]状态码[空格]状态码描述[回车符][换行符] ==》响应行
头部字段[:]值[回车符][换行符] ==》响应头
…
头部字段[:]值[回车符][换行符]
[回车符][换行符] ==》空行
响应数据 ==》响应体HTTP/1.1 200 OK Bdpagetype: 1 Bdqid: 0xf3c9743300024ee4 Cache-Control: private Connection: keep-alive
- HTTP请求方法
- GET:向指定的资源发出“显示”请求。使用 GET 方法应该只用在读取数据,而不应当被用于产生“副作用”的操作中,例如在 Web Application 中。其中一个原因是 GET 可能会被网络蜘蛛等随意访问。
- HEAD:与 GET 方法一样,都是向服务器发出指定资源的请求。只不过服务器将不传回资源的本文部分。它的好处在于,使用这个方法可以在不必传输全部内容的情况下,就可以获取其中“关于该 资源的信息”(元信息或称元数据)。
- POST:向指定资源提交数据,请求服务器进行处理(例如提交表单或者上传文件)。数据被包含 在请求本文中。这个请求可能会创建新的资源或修改现有资源,或二者皆有。
- PUT:向指定资源位置上传其最新内容。
- DELETE:请求服务器删除 Request-URI 所标识的资源。
- TRACE:回显服务器收到的请求,主要用于测试或诊断。
- OPTIONS:这个方法可使服务器传回该资源所支持的所有 HTTP 请求方法。用’*'来代替资源名称, 向 Web 服务器发送 OPTIONS 请求,可以测试服务器功能是否正常运作。
- CONNECT:HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。通常用于SSL加密服 务器的链接(经由非加密的 HTTP 代理服务器)。
- 工作原理
- 客户端连接到 Web 服务器 一个HTTP客户端,通常是浏览器,与 Web 服务器的 HTTP 端口(默认为 80 )建立一个 TCP 套接 字连接。例如,http://www.baidu.com。(URL)
- 发送 HTTP 请求 通过 TCP 套接字,客户端向 Web 服务器发送一个文本的请求报文,一个请求报文由请求行、请求 头部、空行和请求数据 4 部分组成。
- 服务器接受请求并返回 HTTP 响应 Web 服务器解析请求,定位请求资源。服务器将资源复本写到 TCP 套接字,由客户端读取。一个 响应由状态行、响应头部、空行和响应数据 4 部分组成。
- 释放连接 TCP 连接 若 connection 模式为 close,则服务器主动关闭 TCP连接,客户端被动关闭连接,释放 TCP 连 接;若connection 模式为 keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
- 客户端浏览器解析 HTML 内容 客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的 HTML 文档和文档的字符集。客户端浏览器读取响应数据 HTML,根据 HTML
的语法对其进行格式化,并在浏览器窗口中显示。
流程
- 某一个客户端向服务器发起连接请求
- 服务端会与客户端建立请求之后,进入accept状态
- 根据当前的触发模式,调用read_once函数,将数据读取到 m_read_buf 中,
- 服务端调用process函数处理请求报文
- 在处理请求报文的时候,服务端分为主从有限状态机,其中
- 主状态机分为三个状态:读取请求行、读取请求头、读取请求体三种状态,
- 从状态机也分为三个状态:完整行、不完整行、错误语法
- 主状态机从缓冲区中每读取一行数据,都需要交给从状态机判断,同时,根据从状态机的返回结果,主状态机依次进入不同的状态。
- 一开始,主状态处于读取请求行状态,如果在这种状态下读取到完整的数据,证明这些数据属于请求行数据
- 之后,主状态进入读取请求头状态,如果在这种状态下读取到完整的数据,证明这些数据属于请求头数据
- 最后,主状态处于读取请求体状态,如果在这种状态下读取到完整的数据,证明这些数据属于请求体数据
- 当确定接受到一条完整的请求报文(三部分都是完整的)后,就开始处理请求,首先需要获得目标资源的绝对路径
- 根据客户端的请求路径,拼接出资源相对路径 m_real_file,
如 D:\MyCoding\CppProject\下载来的项目代码\linux-server-master\TinyWebServer\root/register.html - 如果当前是 POST请求的页面(POST页面,客户端向服务发送数据),需要和数据库进行互动
- 如果是登录页面
- 将用户名和密码提取出来
- 查询数据库,是否有该条记录?如果有,m_url += “/welcome.html”;如果没有:m_url += “/logError.html” 也就是分别跳转到下一个页面
- 如果当前是注册页面
- 将用户名和密码提取出来
- 查询数据库,看是否有重名(不可以插入主键重复的数据)
- 如果可以注册,向数据库中添加数据,并且m_url += “/log.html” 跳转到 登录页面
- 如果不可以注册,m_url += “/logError.html”,跳转到 注册错误页面
- 如果是登录页面
- 如果当前是 GET请求的页面,不需要与数据库进行互动,直接给客户端发送资源
- 从客户端的请求路径中,定位到目标资源文件名
- 将该目标资源文件名拼接到m_url上,获得一个完整的相对路径
- 至此可以,获得一个目标资源的绝对路径:m_real_file
- 获取目标文件的信息,检验文件的有效性和访问级别
- 打开目标资源文件,将目标文件映射到内存上,准备传输文件
- 根据客户端的请求路径,拼接出资源相对路径 m_real_file,
- 在处理请求报文的时候,服务端分为主从有限状态机,其中
- 使用 wirtev函数对文件进行传输,也就是向客户端发送响应报文
- 判断是否有数据需要传输,如果没有,就将事件重置为EPOLLONESHOT
- 如果有数据需要传输,调用writev(m_sockfd, m_iv, m_iv_count)
- 先发送 m_wirte_buf的数据 – 响应行和响应头
- 再发送 m_real_file的数据 – 响应体-- 目标文件的数据
- 传输完成,解除目标文件的内存映射,将事件重置为EPOLLONESHOT,如果是持续连接,就初始化;
- 这个初始化,指的是初始化所有与下一次C-S交互需要用到的变量。
- 处理结束,关闭服务器
知识点
枚举类型的使用
枚举是 C 语言中的一种基本数据类型,用于定义一组具有离散值的常量。,它可以让数据更简洁,更易读。
枚举类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。
定义一个枚举类型,需要使用 enum 关键字,后面跟着枚举类型的名称,以及用大括号 {} 括起来的一组枚举常量。每个枚举常量可以用一个标识符来表示,也可以为它们指定一个整数值,如果没有指定,那么默认从 0 开始递增。
如何理解有限状态机解析HTTP请求报文
- 主状态机从缓冲区中每读取一行数据,都需要交给从状态机判断,同时,根据从状态机的返回结果,主状态机依次进入不同的状态。
- 一开始,主状态处于读取请求行状态,如果在这种状态下读取到完整的数据,证明这些数据属于请求行数据
- 之后,主状态进入读取请求头状态,如果在这种状态下读取到完整的数据,证明这些数据属于请求头数据
- 最后,主状态处于读取请求体状态,如果在这种状态下读取到完整的数据,证明这些数据属于请求体数据
epoll中边缘触发模式的作用机制
- 使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。
- 因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。
- 所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK。
- 一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。
如何判断行信息的读取状态?
- 首先需要了解报文格式,得知道行结束的条件是 \r\n
- 判断当前当前的字符,是否符合结束条件
erron的认识
学习一些C库函数
- strpbrk
描述
C 库函数 char *strpbrk(const char *str1, const char *str2) 检索字符串 str1 中第一个匹配字符串 str2 中字符的字符,不包含空结束字符。也就是说,依次检验字符串 str1 中的字符,当被检验字符在字符串 str2 中也包含时,则停止检验,并返回该字符位置。
声明
下面是 strpbrk() 函数的声明。
char *strpbrk(const char *str1, const char *str2)
参数
str1 -- 要被检索的 C 字符串。
str2 -- 该字符串包含了要在 str1 中进行匹配的字符列表。
返回值
该函数返回 str1 中第一个匹配字符串 str2 中字符的字符数,如果未找到字符则返回 NULL。
- strcasecmp函数
头文件:#include <string.h>
定义函数:int strncasecmp(const char *str1, const char *str2, size_t n);
函数说明:strncasecmp()用来比较参数str1 和str2 字符串前n个字符,
注意:比较时会自动忽略大小写的差异。
返回值:
(1)若参数str1 和str2 字符串相同则返回0;
(2)str1 若大于str2 则返回大于0 的值;
(3)str1 若小于str2 则返回小于0 的值。
- strspn函数
描述
C 库函数 size_t strspn(const char *str1, const char *str2) 检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。
声明
下面是 strspn() 函数的声明。
size_t strspn(const char *str1, const char *str2)
参数
str1 -- 要被检索的 C 字符串。
str2 -- 该字符串包含了要在 str1 中进行匹配的字符列表。
返回值
该函数返回 str1 中第一个不在字符串 str2 中出现的字符下标。
- strchr函数
strchr() 用于查找字符串中的一个字符,并返回该字符在字符串中第一次出现的位置。
strchr() 其原型定义在头文件 <string.h> 中, char *strchr(const char *str, int c) 在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。
strchr() 函数返回的指针指向字符串中的字符,如果要将该指针用作字符串,应该将其传递给其他字符串处理函数,例如 printf() 或 strncpy()。
声明
下面是 strchr() 函数的声明。
char *strchr(const char *str, int c)
参数
str -- 要查找的字符串。
c -- 要查找的字符。
返回值
如果在字符串 str 中找到字符 c,则函数返回指向该字符的指针,如果未找到该字符则返回 NULL
- strcat函数
描述
C 库函数 char *strcat(char *dest, const char *src) 把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。
声明
下面是 strcat() 函数的声明。
char *strcat(char *dest, const char *src)
参数
dest -- 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串。
src -- 指向要追加的字符串,该字符串不会覆盖目标字符串。
返回值
该函数返回一个指向最终的目标字符串 dest 的指针。
- stat函数
stat函数
作用:获取文件信息
头文件:include <sys/types.h> #include <sys/stat.h> #include <unistd.h>
函数原型:int stat(const char *path, struct stat *buf)
返回值:成功返回0,失败返回-1;
参数:文件路径(名),struct stat 类型的结构体
struct stat 结构体详解:
struct stat
{
dev_t st_dev; /* ID of device containing file */文件使用的设备号
ino_t st_ino; /* inode number */ 索引节点号
mode_t st_mode; /* protection */ 文件对应的模式,文件,目录等
nlink_t st_nlink; /* number of hard links */ 文件的硬连接数
uid_t st_uid; /* user ID of owner */ 所有者用户识别号
gid_t st_gid; /* group ID of owner */ 组识别号
dev_t st_rdev; /* device ID (if special file) */ 设备文件的设备号
off_t st_size; /* total size, in bytes */ 以字节为单位的文件容量
blksize_t st_blksize; /* blocksize for file system I/O */ 包含该文件的磁盘块的大小
blkcnt_t st_blocks; /* number of 512B blocks allocated */ 该文件所占的磁盘块
time_t st_atime; /* time of last access */ 最后一次访问该文件的时间
time_t st_mtime; /* time of last modification */ /最后一次修改该文件的时间
time_t st_ctime; /* time of last status change */ 最后一次改变该文件状态的时间
};
stat结构体中的st_mode 则定义了下列数种情况:
S_IFMT 0170000 文件类型的位遮罩
S_IFSOCK 0140000 套接字
S_IFLNK 0120000 符号连接
S_IFREG 0100000 一般文件
S_IFBLK 0060000 区块装置
S_IFDIR 0040000 目录
S_IFCHR 0020000 字符装置
S_IFIFO 0010000 先进先出
S_ISUID 04000 文件的(set user-id on execution)位
S_ISGID 02000 文件的(set group-id on execution)位
S_ISVTX 01000 文件的sticky位
S_IRUSR(S_IREAD) 00400 文件所有者具可读取权限
S_IWUSR(S_IWRITE)00200 文件所有者具可写入权限
S_IXUSR(S_IEXEC) 00100 文件所有者具可执行权限
S_IRGRP 00040 用户组具可读取权限
S_IWGRP 00020 用户组具可写入权限
S_IXGRP 00010 用户组具可执行权限
S_IROTH 00004 其他用户具可读取权限
S_IWOTH 00002 其他用户具可写入权限
S_IXOTH 00001 其他用户具可执行权限
上述的文件类型在POSIX中定义了检查这些类型的宏定义:
S_ISLNK (st_mode) 判断是否为符号连接
S_ISREG (st_mode) 是否为一般文件
S_ISDIR (st_mode) 是否为目录
S_ISCHR (st_mode) 是否为字符装置文件
S_ISBLK (s3e) 是否为先进先出
S_ISSOCK (st_mode) 是否为socket
若一目录具有sticky位(S_ISVTX),则表示在此目录下的文件只能被该文件所有者、此目录所有者或root来删除或改名,在linux中,最典型的就是这个/tmp目录啦。
st_mode 的结构
st_mode 主要包含了 3 部分信息:
15-12 位保存文件类型
11-9 位保存执行文件时设置的信息
8-0 位保存文件访问权限
- munmap函数 和 mmap 函数
1、mmap函数是一个比较神奇的函数,它可以把文件映射到进程的虚拟内存空间。通过对这段内存的读取和修改,可以实现对文件的读取和修改,而不需要用read和write函数。
2、下面我们来看一下mmap函数的原型
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
在这个函数原型中:
参数addr:指定映射的起始地址,通常设为NULL,由内核来分配
参数length:代表将文件中映射到内存的部分的长度。
参数prot:映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取
参数flags:映射区的特性标志位,常用的两个选项是:
MAP_SHARD:写入映射区的数据会复制回文件,且运行其他映射文件的进程共享
MAP_PRIVATE:对映射区的写入操作会产生一个映射区的复制,对此区域的修改不会写会原文件
参数fd:要映射到内存中的文件描述符,有open函数打开文件时返回的值。
参数offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。
函数返回值:实际分配的内存的起始地址。
3、munmap函数
与mmap函数成对使用的是munmap函数,它是用来解除映射的函数,原型如下:
int munmap(void *start, size_t length)
在这个函数中,
参数start:映射的起始地址
参数length:文件中映射到内存的部分的长度
返回值:解除成功返回0,失败返回-1。
4、实例分析
下面是一个mmap使用的实例代码
//打开文件
fd = open("testdata",O_RDWR);
//创建mmap
start = (char *)mmap(NULL,128,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
//读取文件
strcpy(buf,start);
printf("%s\n",buf);
//写入文件
strcpy(start,"Write to file!\n");
munmap(start,128);
close(fd);
这段代码实现了将测试文件testdata打开,并用mmap函数将文件映射到虚拟内存中,通过指针start对文件进行读写。在终端中可看到由文件读取的数据。程序结束后,可以查看testdata文件,来查看写入的数据。
- writev函数
read/write:
因为使用read()将数据读到不连续的内存、使用write()将不连续的内存发送出去,要经过多次的调用read、write。
如果要从文件中读一片连续的数据至进程的不同区域,有两种方案:
①使用read()一次将它们读至一个较大的缓冲区中,然后将它们分成若干部分复制到不同的区域;
②调用read()若干次分批将它们读至不同区域。
但是多次系统调用+拷贝会带来较大的开销,所以UNIX提供了另外两个函数—readv()和writev(),它们只需一次系统调用就可以实现在文件和进程的多个缓冲区之间传送数据,免除了多次系统调用或复制数据的开销。
2.readv/writev
在一次函数调用中:
① writev以顺序iov[0]、iov[1]至iov[iovcnt-1]从各缓冲区中聚集输出数据到fd。
② readv则将从fd读入的数据按同样的顺序散布到各缓冲区中,readv总是先填满一个缓冲区,然后再填下一个。
3. iovec结构体
#include <sys/uio.h>
struct iovec {
ptr_t iov_base; /* Starting address */
size_t iov_len; /* Length in bytes */
};
struct iovec定义了一个向量元素。通常,这个结构用作一个多元素的数组。对于每一个传输的元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是readv所接收的数据或是writev将要发送的数据。成员iov_len在各种情况下分别确定了接收的最大长度以及实际写入的长度。
int readv(int fd, const struct iovec *vector, int count);
int writev(int fd, const struct iovec *vector, int count);
#include <stdio.h>
2 #include <sys/uio.h>
3
4 int main()
5 {
6 static char part2[] = "THIS IS FROM WRITEV";
7 static int part3 = 65;
8 static char part1[] = "[";
9
10 struct iovec iov[3];
11
12 iov[0].iov_base = part1;
13 iov[0].iov_len = strlen(part1);
14
15 iov[1].iov_base = part2;
16 iov[1].iov_len = strlen(part2);
17
18 iov[2].iov_base = &part3;
19 iov[2].iov_len = sizeof(int);
20
21 writev(1, iov, 3);
22
23 return 0;
24
25 }
- va_list :
<1>原型: void va_start(va_list arg_ptr,prev_param);
功能:以固定参数的地址为起点确定变参的内存起始地址,获取第一个参数的首地址
返回值:无
<2>原型:va_list 类型的变量,va_list arg_ptr ,这个变量是指向参数地址的指针,因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。
<3>原型:type va_arg(va_list arg_ptr,type);
功能:获取下一个参数的地址
返回值:根据传入参数类型决定返回值类型
<4>原型:void va_end(va_list arg_ptr);
功能:将arg_ptr指针置0
返回值:无
- vsnprintf函数
作用:使用vsnprintf()用于向一个字符串缓冲区打印格式化字符串,且可以限定打印的格式化字符串的最大长度。
此函数需要C99或者C++11及以上版本才能支持。
int vsnprintf (char * sbuf, size_t n, const char * format, va_list arg );
参数sbuf:用于缓存格式化字符串结果的字符数组
参数n:限定最多打印到缓冲区sbuf的字符的个数为n-1个,因为vsnprintf还要在结果的末尾追加\0。如果格式化字符串长度大于n-1,则多出的部分被丢弃。如果格式化字符串长度小于等于n-1,则可以格式化的字符串完整打印到缓冲区sbuf。一般这里传递的值就是sbuf缓冲区的长度。
参数format:格式化限定字符串
参数arg:可变长度参数列表
返回:成功打印到sbuf中的字符的个数,不包括末尾追加的\0。如果格式化解析失败,则返回负数。