TeamTalk源码分析之msfs

客户端以http的方式来上传和下载聊天图片。

可能很多同学对http协议不是很熟悉,或者说一知半解。这里大致介绍一下http协议,http协议其实也是一种应用层协议,建立在tcp/ip层之上,其由包头和包体两部分组成(不一定要有包体),看个例子:

比如当我们用浏览器请求一个网址http://www.hootina.org/index.PHP,实际是浏览器给特定的服务器发送如下数据包,包头部分如下:

GET /index.php HTTP/1.1\r\n
Host: www.hootina.org\r\n
Connection: keep-alive\r\n
Cache-Control: max-age=0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n
User-Agent: Mozilla/5.0\r\n
\r\n

这个包没有包体。

从上面我们可以看出一个http协议大致格式可以描述如下:

[plain] view plain copy

  1. GET或Post请求方法  请求的资源路径 http协议版本号\r\n  
  2. 字段名1:值1\r\n  
  3. 字段名2:值2\r\n  
  4. 字段名3:值3\r\n  
  5. 字段名4:值4\r\n  
  6. 字段名5:值5\r\n  
  7. 字段名6:值6\r\n  
  8. \r\n  

 

 

也就是是http协议的头部是一行一行的,每一行以\r\n表示该行结束,最后多出一个空行以\r\n结束表示头部的结束。接下来就是包体的大小了(如果有的话,上文的例子没有包体)。一般get方法会将参数放在请求的资源路径后面,像这样

http://wwww.hootina.org/index.php?变量1=值1&变量2=值2&变量3=值3&变量4=值4

网址后面的问号表示参数开始,每一个参数与参数之间用&隔开

还有一种post的请求方法,这种数据就是将数据放在包体里面了,例如:

 

[plain] view plain copy

  1. POST /otn/login/loginAysnSuggest HTTP/1.1\r\n  
  2. Host: kyfw.12306.cn\r\n  
  3. Connection: keep-alive\r\n  
  4. Content-Length: 96\r\n  
  5. Accept: */*\r\n  
  6. Origin: https://kyfw.12306.cn\r\n  
  7. X-Requested-With: XMLHttpRequest\r\n  
  8. User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75\r\n   
  9. Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n  
  10. Referer: https://kyfw.12306.cn/otn/login/init\r\n  
  11. Accept-Encoding: gzip, deflate, br\r\n  
  12. Accept-Language: zh-CN,zh;q=0.8\r\n  
  13. \r\n  
  14. loginUserDTO.user_name=balloonwj%40qq.com&userDTO.password=xxxxgjqf&randCode=184%2C55%2C37%2C117  


上述报文中loginUserDTO.user_name=balloonwj%40qq.com&userDTO.password=2032_scsgjqf&randCode=184%2C55%2C37%2C117其实包体内容,这个包是我的一个12306买票软件发给12306服务器的报文。这里拿来做个例子。

 

因为对方收到http报文的时候,如果包体有内容,那么必须告诉对方包体有多大。这个最常用的就是通过包头的Content-Length字段来指定大小。上面的例子中Content-Length等于96,正好就是字符串loginUserDTO.user_name=balloonwj%40qq.com&userDTO.password=xxxxgjqf&randCode=184%2C55%2C37%2C117的长度,也就是包体的大小。

还有一种叫做http chunk的编码技术,通过对http包内容进行分块传输。这里就不介绍了(如果你感兴趣,可以私聊我)。

常见的对http协议有如下几个误解:

1. html文档的头就是http的头

 这种认识是错误的,html文档的头部也是http数据包的包体的一部分。正确的http头是长的像上文介绍的那种。

2. 关于http头Connection:keep-alive字段

  一端指定了这个字段后,发http包给另外一端。这个选项只是一种建议性的选项,对端不一定必须采纳,对方也可能在实际实现时,将http连接设置为短连接,即不采纳这个字段的建议。

3. 每个字段都是必须的吗?

不是,大多数字段都不是必须的。但是特定的情况下,某些字段是必须的。比如,通过post发送的数据,就必须设置Content-Length。不然,收包的一端如何知道包体多大。又比如如果你的数据采取了gzip压缩格式,你就必须指定Accept-Encoding: gzip,然对方如何解包你的数据。

好了,http协议就暂且介绍这么多,下面回到正题上来说msfs的源码。

msfs在main函数里面做了如下初始化工作,伪码如下:

 

[cpp] view plain copy

  1. //1. 建立一个两个任务队列,分别处理http get请求和post请求  
  2.   
  3. //2. 创建名称为000~255的文件夹,每个文件夹里面会有000~255个子目录,这些目录用于存放聊天图片  
  4.   
  5. //3. 在8700端口上监听客户端连接  
  6.   
  7. //4. 启动程序消息泵  


 

 

第1点,建立任务队列我们前面系列的文章已经介绍过了。

第2点,代码如下:

 

[cpp] view plain copy

  1. g_fileManager = FileManager::getInstance(listen_ip, base_dir, fileCnt, filesPerDir);  
  2. int ret = g_fileManager->initDir();  

 

 

[cpp] view plain copy

  1. int FileManager::initDir() {  
  2.         bool isExist = File::isExist(m_disk);  
  3.         if (!isExist) {  
  4.             u64 ret = File::mkdirNoRecursion(m_disk);  
  5.             if (ret) {  
  6.                 log("The dir[%s] set error for code[%d], \  
  7.                     its parent dir may no exists", m_disk, ret);  
  8.                 return -1;  
  9.             }  
  10.         }  
  11.           
  12.         //255 X 255   
  13.         char first[10] = {0};  
  14.         char second[10] = {0};  
  15.         for (int i = 0; i <= FIRST_DIR_MAX; i++) {  
  16.             snprintf(first, 5, "%03d", i);  
  17.             string tmp = string(m_disk) + "/" + string(first);  
  18.             int code = File::mkdirNoRecursion(tmp.c_str());  
  19.             if (code && (errno != EEXIST)) {  
  20.                 log("Create dir[%s] error[%d]", tmp.c_str(), errno);  
  21.                 return -1;  
  22.             }  
  23.             for (int j = 0; j <= SECOND_DIR_MAX; j++) {  
  24.                 snprintf(second, 5, "%03d", j);  
  25.                 string tmp2 = tmp + "/" + string(second);  
  26.                 code = File::mkdirNoRecursion(tmp2.c_str());  
  27.                 if (code && (errno != EEXIST)) {  
  28.                     log("Create dir[%s] error[%d]", tmp2.c_str(), errno);  
  29.                     return -1;  
  30.                 }  
  31.                 memset(second, 0x0, 10);  
  32.             }  
  33.             memset(first, 0x0, 10);  
  34.         }  
  35.           
  36.         return 0;  
  37.     }  



 

下面,我们直接来看如何处理客户端的http请求,当连接对象CHttpConn收到客户端数据后,调用OnRead方法:

 

[cpp] view plain copy

  1. void CHttpConn::OnRead()  
  2. {  
  3.     for (;;)  
  4.     {  
  5.         uint32_t free_buf_len = m_in_buf.GetAllocSize()  
  6.                 - m_in_buf.GetWriteOffset();  
  7.         if (free_buf_len < READ_BUF_SIZE + 1)  
  8.             m_in_buf.Extend(READ_BUF_SIZE + 1);  
  9.   
  10.         int ret = netlib_recv(m_sock_handle,  
  11.                 m_in_buf.GetBuffer() + m_in_buf.GetWriteOffset(),  
  12.                 READ_BUF_SIZE);  
  13.         if (ret <= 0)  
  14.             break;  
  15.   
  16.         m_in_buf.IncWriteOffset(ret);  
  17.   
  18.         m_last_recv_tick = get_tick_count();  
  19.     }  
  20.   
  21.     // 每次请求对应一个HTTP连接,所以读完数据后,不用在同一个连接里面准备读取下个请求  
  22.     char* in_buf = (char*) m_in_buf.GetBuffer();  
  23.     uint32_t buf_len = m_in_buf.GetWriteOffset();  
  24.     in_buf[buf_len] = '\0';  
  25.   
  26.     //log("OnRead, buf_len=%u, conn_handle=%u", buf_len, m_conn_handle); // for debug  
  27.   
  28.   
  29.     m_HttpParser.ParseHttpContent(in_buf, buf_len);  
  30.   
  31.     if (m_HttpParser.IsReadAll())  
  32.     {  
  33.         string strUrl = m_HttpParser.GetUrl();  
  34.         log("IP:%s access:%s", m_peer_ip.c_str(), strUrl.c_str());  
  35.         if (strUrl.find("..") != strUrl.npos) {  
  36.             Close();  
  37.             return;  
  38.         }  
  39.         m_access_host = m_HttpParser.GetHost();  
  40.         if (m_HttpParser.GetContentLen() > HTTP_UPLOAD_MAX)  
  41.         {  
  42.             // file is too big  
  43.             log("content  is too big");  
  44.             char url[128];  
  45.             snprintf(url, sizeof(url), "{\"error_code\":1,\"error_msg\": \"上传文件过大\",\"url\":\"\"}");  
  46.             log("%s",url);  
  47.             uint32_t content_length = strlen(url);  
  48.             char pContent[1024];  
  49.             snprintf(pContent, sizeof(pContent), HTTP_RESPONSE_HTML, content_length,url);  
  50.             Send(pContent, strlen(pContent));  
  51.             return;  
  52.         }  
  53.   
  54.         int nContentLen = m_HttpParser.GetContentLen();  
  55.         char* pContent = NULL;  
  56.         if(nContentLen != 0)  
  57.         {  
  58.             try {  
  59.                 pContent =new char[nContentLen];  
  60.                 memcpy(pContent, m_HttpParser.GetBodyContent(), nContentLen);  
  61.             }  
  62.             catch(...)  
  63.             {  
  64.                 log("not enough memory");  
  65.                 char szResponse[HTTP_RESPONSE_500_LEN + 1];  
  66.                 snprintf(szResponse, HTTP_RESPONSE_500_LEN, "%s", HTTP_RESPONSE_500);  
  67.                 Send(szResponse, HTTP_RESPONSE_500_LEN);  
  68.                 return;  
  69.             }  
  70.         }  
  71.         Request_t request;  
  72.         request.conn_handle = m_conn_handle;  
  73.         request.method = m_HttpParser.GetMethod();;  
  74.         request.nContentLen = nContentLen;  
  75.         request.pContent = pContent;  
  76.         request.strAccessHost = m_HttpParser.GetHost();  
  77.         request.strContentType = m_HttpParser.GetContentType();  
  78.         request.strUrl = m_HttpParser.GetUrl() + 1;  
  79.         CHttpTask* pTask = new CHttpTask(request);  
  80.         if(HTTP_GET == m_HttpParser.GetMethod())  
  81.         {  
  82.             g_GetThreadPool.AddTask(pTask);  
  83.         }  
  84.         else  
  85.         {  
  86.             g_PostThreadPool.AddTask(pTask);  
  87.         }  
  88.     }  
  89. }  


该方法先收取数据,接着解包,然后根据客户端发送的http请求到底是get还是post方法,分别往对应的get和post任务队列中丢一个任务CHttpTask。任务队列开始处理这个任务。我们以get请求的任务为例(Post请求与此类似):

 

 

[cpp] view plain copy

  1. void CHttpTask::run()  
  2. {  
  3.   
  4.     if(HTTP_GET == m_nMethod)  
  5.     {  
  6.         OnDownload();  
  7.     }  
  8.     else if(HTTP_POST == m_nMethod)  
  9.     {  
  10.        OnUpload();  
  11.     }  
  12.     else  
  13.     {  
  14.         char* pContent = new char[strlen(HTTP_RESPONSE_403)];  
  15.         snprintf(pContent, strlen(HTTP_RESPONSE_403), HTTP_RESPONSE_403);  
  16.         CHttpConn::AddResponsePdu(m_ConnHandle, pContent, strlen(pContent));  
  17.     }  
  18.     if(m_pContent != NULL)  
  19.     {  
  20.         delete [] m_pContent;  
  21.         m_pContent = NULL;  
  22.     }  
  23. }  


处理任务时,根据请求类型判断到底是客户端下载图片还是上传图片,如果是下载图片则从本机缓存的图片信息中找到该图片,并读取该图片数据,因为是聊天图片,所以一般不会很大,所以这里都是一次性读取图片字节内容,然后发出去。

 

 

[cpp] view plain copy

  1. void  CHttpTask::OnDownload()  
  2. {  
  3.         uint32_t  nFileSize = 0;  
  4.         int32_t nTmpSize = 0;  
  5.         string strPath;  
  6.         if(g_fileManager->getAbsPathByUrl(m_strUrl, strPath ) == 0)  
  7.         {  
  8.             nTmpSize = File::getFileSize((char*)strPath.c_str());  
  9.             if(nTmpSize != -1)  
  10.             {  
  11.                 char szResponseHeader[1024];  
  12.                 size_t nPos = strPath.find_last_of(".");  
  13.                 string strType = strPath.substr(nPos + 1, strPath.length() - nPos);  
  14.                 if(strType == "jpg" || strType == "JPG" || strType == "jpeg" || strType == "JPEG" || strType == "png" || strType == "PNG" || strType == "gif" || strType == "GIF")  
  15.                 {  
  16.                     snprintf(szResponseHeader, sizeof(szResponseHeader), HTTP_RESPONSE_IMAGE, nTmpSize, strType.c_str());  
  17.                 }  
  18.                 else  
  19.                 {  
  20.                     snprintf(szResponseHeader,sizeof(szResponseHeader), HTTP_RESPONSE_EXTEND, nTmpSize);  
  21.                 }  
  22.                 int nLen = strlen(szResponseHeader);  
  23.                 char* pContent = new char[nLen + nTmpSize];  
  24.                 memcpy(pContent, szResponseHeader, nLen);  
  25.                 g_fileManager->downloadFileByUrl((char*)m_strUrl.c_str(), pContent + nLen, &nFileSize);  
  26.                 int nTotalLen = nLen + nFileSize;  
  27.                 CHttpConn::AddResponsePdu(m_ConnHandle, pContent, nTotalLen);  
  28.             }  
  29.             else  
  30.             {  
  31.                 int nTotalLen = strlen(HTTP_RESPONSE_404);  
  32.                 char* pContent = new char[nTotalLen];  
  33.                 snprintf(pContent, nTotalLen, HTTP_RESPONSE_404);  
  34.                 CHttpConn::AddResponsePdu(m_ConnHandle, pContent, nTotalLen);  
  35.                 log("File size is invalied\n");  
  36.                   
  37.             }  
  38.         }  
  39.         else  
  40.         {  
  41.             int nTotalLen = strlen(HTTP_RESPONSE_500);  
  42.             char* pContent = new char[nTotalLen];  
  43.             snprintf(pContent, nTotalLen, HTTP_RESPONSE_500);  
  44.             CHttpConn::AddResponsePdu(m_ConnHandle, pContent, nTotalLen);  
  45.         }  
  46. }  


 

 

这里需要说明一下的就是FileManager::getAbsPathByUrl在获取本地文件时,用了一个锁,该锁是为了防止同一个进程同时读取同一个文件,这个锁是“建议性”的,必须自己主动检测有没有上锁:

 

[cpp] view plain copy

  1. int FileManager::getAbsPathByUrl(const string &url, string &path) {  
  2.     string relate;  
  3.     if (getRelatePathByUrl(url, relate)) {  
  4.         log("Get path from url[%s] error", url.c_str());  
  5.         return -1;  
  6.     }  
  7.     path = string(m_disk) + relate;  
  8.     return 0;  
  9. }  

 

[cpp] view plain copy

  1. u64 File::open(bool directIo) {  
  2.     assert(!m_opened);  
  3.     int flags = O_RDWR;  
  4. #ifdef __linux__      
  5.     m_file = open64(m_path, flags);  
  6. #elif defined(__FREEBSD__) || defined(__APPLE__)  
  7.     m_file = ::open(m_path, flags);  
  8. #endif    
  9.     if(-1 == m_file) {  
  10.         return errno;  
  11.     }  
  12. #ifdef __LINUX__  
  13.     if (directIo)  
  14.         if (-1 == fcntl(m_file, F_SETFL, O_DIRECT))  
  15.             return errno;   
  16. #endif    
  17.     struct flock lock;  
  18.     lock.l_type = F_WRLCK;  
  19.     lock.l_start = 0;  
  20.     lock.l_whence = SEEK_SET;  
  21.     lock.l_len = 0;  
  22.     if(fcntl(m_file, F_SETLK, &lock) < 0) {  
  23.         ::close(m_file);  
  24.         return errno;  
  25.     }  
  26.   
  27.     m_opened = true;  
  28.     u64 size = 0;  
  29.     u64 code = getSize(&size);  
  30.     if (code) {  
  31.         close();  
  32.         return code;  
  33.     }  
  34.     m_size = size;  
  35.     m_directIo = directIo;  
  36.     return 0;  
  37. }  


 

 

注意上面的fcntl函数设置的flock锁。这个是Linux特有的,应该学习掌握。

 

图片上传的逻辑和下载逻辑大致类似,这里就不再分析了。

 

当然,发送图片数据的包和前面的发送逻辑也是一样的,在OnWrite里面发送。发送完毕后会调用CHttpConn::OnSendComplete,在这个函数里面关闭http连接。这也就是说msfs与客户端的http连接也是短连接。

 

[cpp] view plain copy

  1. void CHttpConn::OnSendComplete()  
  2. {  
  3.     Close();  
  4. }  


 

 

关于msfs也就这么多内容了。不知道你有没有发现,在搞清楚db_proxy_server和msg_server之后,每个程序框架其实都是一样的,只不过业务逻辑稍微有一点差别。后面介绍的file_server和route_server都是一样的。我们也着重分析其业务代码。

 

好了,msfs服务就这么多啦。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值