前言
個人覺得開發中比較重要的一點就是“不要重複發明輪子”,現在國外、國內開源軟件遍地開花,尤其是Google,開源了一系列性能、架構特別好的代碼,如果能夠用開源的應該儘量避免自己造輪子。那麼爲什麼不用log4plus呢?在這裏我需要的是一個簡單實用、輕巧的日誌記錄程序,log4plus對我有點臃腫,所以才自己花店時間寫了一個簡單的日誌記錄類。
日誌類實現
剛開始想的是爲了避免大量的讀寫程序影響性能,可以保存一個大的緩存,每次寫日誌就往緩存中追加數據。緩存滿了後,就把緩存數據寫入文件中並清空緩存。這個優點很明顯:I/O操作是很耗時的,寫日誌太過頻繁,缺點:一旦日誌程序異常退出,前面寫的日誌可能流失。那麼,對應的另一種方法就是:每次寫日誌就打開文件寫入數據,寫完關閉文件,頻繁操作I/O,對於性能沒那麼高要求的也還好。
然後就是多個線程間互斥的設置,在同一時間對一個文件操作的當然只能有一個文件。Windows上線程同步方法:臨界區、事件、信號量,我這裏用的是臨界區。
日誌記錄程序實現
多個進程公用一個日誌記錄程序寫日誌,因此日誌程序必須相當穩定,還要處理進程間同步的問題。我使用的是發送WM_COPYDATA消息,日誌進程的消息循環會對所有進程的日誌消息放入消息隊列(先入先出),無需我們去處理互斥。消息循環接收到消息後,把日誌數據拷貝到內存中,然後將這個數據發送到日誌記錄線程中。所有的日誌都在一個單獨的工作線程中寫入文件,該線程也維護着自己的一個消息循環,處理窗口消息接收WM_COPYDATA後發送來的日誌數據。同樣的道理,消息循環已經爲我們維護了先進先出的數據結構,我們不用考慮互斥加鎖的問題,一個接着一個的寫入文件就OK了。
日誌類代碼
/********************************************
*一個簡單的日誌記錄類
*2015年7月28日
*Jelin
*mailto://[email protected]
*/
#pragma once
#include "../Lock.h"
//文件最大4M
static const int g_nMaxSize = 1024*1024*4;
class CLog
{
public:
static CLog* Instance()
{
static CLog log;
return &log;
}
bool WriteLog(const char* lpLog);
bool WriteLog(const wchar_t* lpLog);
bool WriteLog(const char* lpFile, long lLine, const char* lpLog);
bool WriteLog(const char* lpFile, long lLine, const wchar_t* lpLog);
bool WriteJoinLog(const wchar_t* lpText, const wchar_t* lpLog);
bool WriteJoinLog(const char* lpFile, long lLine, const wchar_t* lpText, const wchar_t* lpLog);
protected:
CLog();
~CLog();
bool InitLogFile();
char* WcharToChar(const wchar_t* lpSource);
private:
wchar_t m_szLog[MAX_PATH];
int m_nWriteSize;
CLock m_lock;
};
#define SLOG1(x) CLog::Instance()->WriteLog(x);
#define SLOG2(x, y) CLog::Instance()->WriteJoinLog(x, y);
#define LOG1(x) CLog::Instance()->WriteLog(__FILE__, __LINE__, x)
#define LOG2(x, y) CLog::Instance()->WriteJoinLog(__FILE__, __LINE__, x, y)
#include "stdafx.h"
#include "Log.h"
#include <time.h>
#include <ShlObj.h>
static const char* g_lpEnd = "\n";
CLog::CLog()
: m_nWriteSize(0)
{
InitLogFile();
}
CLog::~CLog()
{
}
bool CLog::InitLogFile()
{
wchar_t szPath[MAX_PATH]={0};
::GetModuleFileName(NULL, szPath, MAX_PATH);
for ( int i=wcslen(szPath)-1; i>=0; --i )
{
if ( szPath[i] == '\\' )
break;
szPath[i] = '\0';
}
wcscat(szPath, L"Log");
SHCreateDirectory(NULL, szPath);
SYSTEMTIME st;
GetLocalTime(&st);
swprintf(m_szLog, L"%s\\%d%02d%02d-%02d%02d%02d%03d.log", szPath, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
m_nWriteSize = 0;
return true;
}
bool CLog::WriteLog( const char* lpLog )
{
if ( NULL == lpLog )
return false;
int nLen = strlen(lpLog);
if ( 0 == nLen )
return true;
CScopeLock sl(m_lock);
if ( m_nWriteSize>=g_nMaxSize )
InitLogFile();
FILE* fp = _wfopen(m_szLog, L"a+");
if ( NULL == fp )
{
OutputDebugString(L"打開日誌文件失敗");
return false;
}
SYSTEMTIME st;
::GetLocalTime(&st);
char szTime[30]={0};
sprintf(szTime, "%d-%02d-%02d %02d:%02d:%02d\n", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
fwrite(szTime, 1, strlen(szTime), fp);
fwrite(lpLog, 1, nLen, fp);
fwrite(g_lpEnd, 1, 1, fp);
fclose(fp);
m_nWriteSize += nLen+20;
return true;
}
bool CLog::WriteLog( const wchar_t* lpLog )
{
char* pBuffer = WcharToChar(lpLog);
if ( NULL == pBuffer )
return false;
bool bRet = WriteLog(pBuffer);
free(pBuffer);
return bRet;
}
bool CLog::WriteLog( const char* lpFile, long lLine, const char* lpLog )
{
if ( NULL == lpLog )
return false;
int nLen = strlen(lpLog);
if ( 0 == nLen )
return true;
CScopeLock sl(m_lock);
if ( m_nWriteSize>=g_nMaxSize )
InitLogFile();
FILE* fp = _wfopen(m_szLog, L"a+");
if ( NULL == fp )
{
OutputDebugString(L"打開日誌文件失敗");
return false;
}
SYSTEMTIME st;
::GetLocalTime(&st);
char szTime[30]={0};
sprintf(szTime, "%d-%02d-%02d %02d:%02d:%02d\n", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
fwrite(szTime, 1, strlen(szTime), fp);
if ( NULL != lpFile )
fwrite(lpFile, 1, strlen(lpFile), fp);
char szLine[30];
sprintf_s(szLine, " line=%ld:", lLine);
fwrite(szLine, 1, strlen(szLine), fp);
fwrite(lpLog, 1, nLen, fp);
fwrite(g_lpEnd, 1, 1, fp);
fclose(fp);
m_nWriteSize += nLen+20;
return true;
}
bool CLog::WriteLog( const char* lpFile, long lLine, const wchar_t* lpLog )
{
char* lpBuffer = WcharToChar(lpLog);
if ( NULL == lpBuffer )
return false;
bool bRet = WriteLog(lpFile, lLine, lpBuffer);
free(lpBuffer);
return bRet;
}
char* CLog::WcharToChar( const wchar_t* lpSource )
{
if ( NULL == lpSource )
return NULL;
int nLen = wcslen(lpSource);
if ( 0 == nLen )
return NULL;
int nNeedSize = WideCharToMultiByte(CP_ACP, 0, lpSource, nLen, NULL, 0, NULL, NULL);
if ( 0 == nNeedSize )
return NULL;
char* pBuffer = (char*)malloc(sizeof(char)*(nNeedSize+1));
if ( NULL == pBuffer )
return NULL;
WideCharToMultiByte(CP_ACP, 0, lpSource, nLen, pBuffer, nNeedSize, NULL, NULL);
pBuffer[nNeedSize] = '\0';
return pBuffer;
}
bool CLog::WriteJoinLog( const wchar_t* lpText, const wchar_t* lpLog )
{
if ( NULL == lpText || NULL == lpLog )
return false;
int nTextLen = wcslen(lpText);
int nLogLen = wcslen(lpLog);
wchar_t* lpBuffer = (wchar_t*)malloc(sizeof(wchar_t)*(nTextLen+nLogLen+1));
wcscpy(lpBuffer, lpText);
wcscpy(lpBuffer+nTextLen, lpLog);
lpBuffer[nTextLen+nLogLen] = '\0';
bool bRet = WriteLog(lpBuffer);
free(lpBuffer);
return bRet;
}
bool CLog::WriteJoinLog( const char* lpFile, long lLine, const wchar_t* lpText, const wchar_t* lpLog )
{
if ( NULL == lpFile || NULL == lpText || NULL == lpLog )
return false;
int nTextLen = wcslen(lpText);
int nLogLen = wcslen(lpLog);
wchar_t* lpBuffer = (wchar_t*)malloc(sizeof(wchar_t)*(nTextLen+nLogLen+1));
wcscpy(lpBuffer, lpText);
wcscpy(lpBuffer+nTextLen, lpLog);
lpBuffer[nTextLen+nLogLen] = '\0';
bool bRet = WriteLog(lpFile, lLine, lpBuffer);
free(lpBuffer);
return bRet;
}
注意,文件操作最好只在一個函數裏處理,便於統一管理(加鎖、解鎖)。
日誌程序代碼(win32程序)
// Logger.cpp : 定義應用程序的入口點。
//
#include "stdafx.h"
#include "Logger.h"
#include <atlstr.h>
enum{
WM_MSG_LOGGER = WM_USER + 200,
};
// 全局變量:
HINSTANCE hInst;
HWND g_hMainWnd = NULL;
HWND g_hServerWnd = NULL;
HANDLE g_hLogTh = NULL;
UINT g_dwLogTid = 0;
// 此代碼模塊中包含的函數的前向聲明:
HWND InitInstance(HINSTANCE);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
UINT __stdcall LogThread(void* lpParam);
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
try
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
if ( wcslen(lpCmdLine) == 0 )
{
TRACEW(L"參數爲空\n");
return 0;
}
wstring strCmd(lpCmdLine);
string strJson = U2A(lpCmdLine);
Json::Reader r;
Json::Value vRoot;
if ( !r.parse(strJson, vRoot) )
{
TRACEW(L"參數格式不正確,解析失敗\n");
return 0;
}
g_hServerWnd = (HWND)vRoot["wnd"].asInt();
g_hMainWnd = InitInstance (hInstance);
if ( NULL == g_hMainWnd )
{
TRACEW(L"創建消息窗口失敗\n");
return 0;
}
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
catch(std::exception e)
{
TRACEA("捕獲到異常信息:%s\n", e.what());
}
catch(...)
{
TRACEW(L"捕獲到未知異常");
}
return 0;
}
HWND InitInstance(HINSTANCE hInstance)
{
static const wchar_t kWndClass[] = L"ClientMessageWindow";
WNDCLASSEX wc = {0};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = kWndClass;
RegisterClassEx(&wc);
return CreateWindow(kWndClass, 0, 0, 0, 0, 0, 0, HWND_MESSAGE, 0, hInstance, 0);
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_CREATE:
{
//創建日誌記錄線程
g_hLogTh = (HANDLE)_beginthreadex(NULL, 0, LogThread, NULL, 0, &g_dwLogTid);
//通知發送方,日誌記錄進程的主窗口句柄
::PostMessage(g_hServerWnd, WM_MSG_LOGGER, 0, (LPARAM)hWnd);
//設置定時器,檢測發送進程的狀態
::SetTimer(hWnd, 1, 3000, NULL);
break;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_COPYDATA:
{
COPYDATASTRUCT* lpData = (COPYDATASTRUCT*)lParam;
/*收到日誌消息後,我們需要立即處理返回,因爲發送方還在阻塞等待
/*於是拷貝數據到內存中,將這個內存的地址投遞到寫日誌的線程隊列中去
/*這裏會頻繁的 申請\釋放內存,於是用到了tcmalloc,提高性能
*/
char* pLog = (char*)malloc(lpData->cbData+1);
memcpy(pLog, lpData->lpData, lpData->cbData);
pLog[lpData->cbData] = '\0';
::PostThreadMessage(g_dwLogTid, WM_MSG_LOGGER, 0, (LPARAM)pLog);
break;
}
case WM_TIMER:
{
//檢測發送方窗口是否存在,不存在說明發送日誌進程已經退出
if ( !IsWindow(g_hServerWnd) )
{
::KillTimer(hWnd, 1);
//先退出日誌線程
::PostThreadMessage(g_dwLogTid, WM_QUIT, 0, 0);
WaitForSingleObject(g_hLogTh, 5*1000);
CloseHandle(g_hLogTh);
//關閉主窗口,退出進程
::PostMessage(hWnd, WM_CLOSE, 0, 0);
}
break;
}
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
//日誌記錄線程
UINT __stdcall LogThread(void* lpParam)
{
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
if ( msg.message == WM_MSG_LOGGER )
{//由於消息隊列替我們維護了先進先出的模型,這裏寫日誌時無需加鎖
char* pLog = (char*)msg.lParam;
SLOG1(pLog);
free(pLog);
}
}
return 0;
}
每次寫入文件都會記錄下寫入大小,寫入大小達到設定的最大值後,程序就會自動再創建一個日誌文件。
還有一個
Lock.h
是一個簡單的封裝類,封裝了下臨界區的初始化,加鎖解鎖操作,使用十分方便。
#pragma once
class CLock
{
public:
CLock(void) { ::InitializeCriticalSection(&m_cs); }
~CLock(void) { ::DeleteCriticalSection(&m_cs); }
void Lock() { ::EnterCriticalSection(&m_cs); }
void UnLock() { ::LeaveCriticalSection(&m_cs); }
private:
CRITICAL_SECTION m_cs;
};
class CScopeLock
{
public:
CScopeLock(CLock& lock)
:m_lock(lock)
{
m_lock.Lock();
}
~CScopeLock()
{
m_lock.UnLock();
}
private:
CLock& m_lock;
};
如何使用
#define SLOG1(x) CLog::Instance()->WriteLog(x);
#define SLOG2(x, y) CLog::Instance()->WriteJoinLog(x, y);
#define LOG1(x) CLog::Instance()->WriteLog(__FILE__, __LINE__, x)
#define LOG2(x, y) CLog::Instance()->WriteJoinLog(__FILE__, __LINE__, x, y)
簡單的日誌記錄,不需要打印文件名和代碼行:
SLOG1(L"日誌第一行");
SLOG1("日誌第二行");
SLOG2("日誌第三行", "第三行補充文字") 用於簡單文字拼接
需要打印文件名和代碼行的:
LOG1("第四行日誌")
LOG2("第五行日誌", "補充說明")
支持ASCII和UNICODE,當然了,UNICODE內部需要轉碼,如果可以選擇當然還是用ASCII效率更高。