对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就是封包和拆包

一、为什么基于TCP的通讯程序需要进行封包和拆包.

  TCP是个"流"协议,所谓流,就是没有界限的一串数据。

  大家可以想想河里的流水,是连成一片的,其间是没有分界线的。但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包。

  由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况:
  假设我们连续调用两次 send 分别发送两段数据 data1 和 data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况):
  A.先接收到 data1,然后接收到 data2。
  B.先接收到 data1 的部分数据,然后接收到 data1 余下的部分以及 data2 的全部。
  C.先接收到了data1 的全部数据和 data2 的部分数据,然后接收到了 data2 的余下的数据。
  D.一次性接收到了data1和data2的全部数据。

  对于A这种情况正是我们需要的,在此不再做讨论。

  对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包。为了拆包就必须在发送端进行封包。
  另外要注意的是:对于UDP来说就不存在拆包的问题。因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收。

二、为什么会出现 B,C,D 的情况?

"粘包"可发生在发送端也可发生在接收端。
1、由Nagle算法造成的发送端的粘包:

Nagle算法是一种改善网络传输效率的算法。简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。这是对Nagle算法一个简单的解释,详细的请看相关书籍。像 C 和 D 的情况就有可能是 Nagle 算法造成的。
2、接收端接收不及时造成的接收端粘包:

TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。

三、怎样封包和拆包?

最初遇到"粘包"的问题时,我是通过在两次 send 之间调用 sleep 来休眠一小段时间来解决。

这个解决方法的缺点是显而易见的:使传输效率大大降低,而且也并不可靠。后来就是通过应答的方式来解决,尽管在大多数时候是可行的,但是不能解决象 B 的那种情况,而且采用应答方式增加了通讯量,加重了网络负荷(但是象FTP等协议采用的就是应答方式)。再后来就是对数据包进行封包和拆包的操作。

封包:
封包就是给一段数据加上包头,这样一来数据包就分为包头包体两部分内容了(以后讲过滤非法包时封包会加入"包尾"内容)。

包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义。根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

拆包:
对于拆包目前我最常用的是以下两种方式:
1、动态缓冲区暂存方式。之所以说缓冲区是动态的是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度。
大概过程描述如下:
A 为每一个连接动态分配一个缓冲区,同时把此缓冲区和 SOCKET 关联,常用的是通过结构体关联。
B 当接收到数据时首先把此段数据存放在缓冲区中。
C 判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作。
D 根据包头数据解析出里面代表包体长度的变量。
E 判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作。
F 取出整个数据包,这里的"取"的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉。删除的办法就是把此包后面的数据移动到缓冲区的起始地址。

这种方法有两个缺点:

1)为每个连接动态分配一个缓冲区增大了内存的使用;

2)有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除。这种拆包的改进方法会解决和完善部分缺点。

下面给出相关代码.

  先看包头结构定义

#pragma pack(push,1) //开始定义数据包, 采用字节对齐方式
/*----------------------包头---------------------*/
typedef struct tagPACKAGEHEAD
{
BYTE Version;
WORD Command;
WORD nDataLen;//包体的长度
}PACKAGE_HEAD;
#pragma pack(pop) //结束定义数据包, 恢复原来对齐方式

  然后看存放数据和“取"数据函数:

/*****************************************************************************
Description:添加数据到缓存
Input:pBuff[in]-待添加的数据;nLen[in]-待添加数据长度
Return: 如果当前缓冲区没有足够的空间存放pBuff则返回FALSE;否则返回TRUE。
******************************************************************************/
bool CDataBufferPool::AddBuff( char *pBuff, int nLen )
{
m_cs.Lock();///临界区锁
if ( nLen < )
{
m_cs.Unlock();
return false;
} if(nLen <= GetFreeSize())///判断剩余空间是否足够存放nLen长的数据
{
memcpy(m_pBuff + m_nOffset, pBuff, nLen);
m_nOffset += nLen;
}
else///若不够则扩充原有的空间
{
char *p = m_pBuff;
m_nSize += nLen*;//每次增长2*nLen
m_pBuff = new char[m_nSize]; memcpy(m_pBuff,p,m_nOffset);
delete []p;
memcpy(m_pBuff + m_nOffset, pBuff, nLen);
m_nOffset += nLen;
m_cs.Unlock();
return false;
}
m_cs.Unlock();
return true;
}
/*****************************************************************************
Description:获取一个完整的包
Input:Buf[out]-获取到的数据;nLen[out]-获取到的数据长度
Return: 1、当前缓冲区不够一个包头的数据 2、当前缓冲区不够一个包体的数据
******************************************************************************/
int CDataBufferPool::GetFullPacket(char *Buf, int& nLen)
{
m_cs.Lock();
if(m_nOffset < m_PacketHeadLen)//当前缓冲区不够一个包头的数据
{
m_cs.Unlock();
return ;
} PACKAGE_HEAD *p = (PACKAGE_HEAD *)m_pBuff;
if((m_nOffset - m_PacketHeadLen) < (int)p->nDataLen)//当前缓冲区不够一个包体的数据
{
m_cs.Unlock();
return ;
} //判断包的合法性
/* int IsIntegrallity = ValidatePackIntegrality(p);
if( IsIntegrallity != 0 )
{
m_cs.Unlock();
return IsIntegrallity;
}
*/
nLen = m_PacketHeadLen+p->nDataLen;
memcpy( Buf, m_pBuff, nLen );
m_nOffset -= nLen;
memcpy( m_pBuff, m_pBuff+nLen, m_nOffset ); m_cs.Unlock();
return ;
}

前面提到过这种方法的缺点。下面给出一个改进办法,即采用环形缓冲。但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方)。第2种拆包方式会解决这两个问题。
环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾。在存放数据和删除数据时只是进行头尾指针的移动。
用代码来说明。

注:下面的代码是采用一个开源的游戏服务器的代码,我对此代码有所修改。

int CCircularBufferPool::PutData(TCHAR *pData, int len)
{
if( len <= )
return ; EnterCriticalSection(&m_cs);
while(IsOverFlowCondition(len))///判断缓冲区剩余空间是否够存放len长的数据
{
BufferResize(len);///若不够,则扩充缓冲区.
} if(IsIndexOverFlow(len))///判断"尾"指针的位置.
{
int FirstCopyLen = m_iBufSize-m_iTailPos;
int SecondCopyLen = len - FirstCopyLen;
CopyMemory(m_pBuffer+m_iTailPos, pData, FirstCopyLen);
if (SecondCopyLen)
{
CopyMemory(m_pBuffer, pData+FirstCopyLen, SecondCopyLen);
m_iTailPos = SecondCopyLen;
}
else
m_iTailPos = ;
}
else
{
CopyMemory(m_pBuffer+m_iTailPos, pData, len);
m_iTailPos += len;
} LeaveCriticalSection(&m_cs);
return ;
} void CCircularBufferPool::GetData(TCHAR *pData, int len, bool Delete)
{
if(len < m_iBufSize-m_iHeadPos)
{
CopyMemory(pData, m_pBuffer+m_iHeadPos, len);
if(Delete==true)
m_iHeadPos += len;
}
else
{
int fc, sc;
fc = m_iBufSize-m_iHeadPos;
sc = len - fc;
CopyMemory(pData, m_pBuffer+m_iHeadPos, fc);
if (sc)
CopyMemory(pData+fc, m_pBuffer, sc);
if(Delete==true)
m_iHeadPos = sc;
if(m_iHeadPos >= m_iBufSize)
m_iHeadPos = ;
}
} //
//进行自定义包的解析
//
int CCircularBufferPool::GetFullPacket( TCHAR *Buf, int &nLen )
{
EnterCriticalSection(&m_cs);
if(GetValidCount() < m_PacketHeadLen )//当前缓冲区不够一个包头的数据
{
LeaveCriticalSection(&m_cs);
return ;
} GetData(Buf,m_PacketHeadLen,false);
PACKAGE_HEAD *p = (PACKAGE_HEAD *)Buf;
if( (GetValidCount()-m_PacketHeadLen) < (int)p->nDataLen )//当前缓冲区不够一个包体的数据
{
LeaveCriticalSection(&m_cs);
return ;
} //判断包的合法性
int IsIntegrallity = ValidatePackIntegrality(p);
if( IsIntegrallity != )
{
LeaveCriticalSection(&m_cs);
return IsIntegrallity;
} GetData(Buf,m_PacketHeadLen+p->nDataLen,true);
nLen = m_PacketHeadLen+p->nDataLen; LeaveCriticalSection(&m_cs);
return ;
}
int CCircularBufferPool::PutData(TCHAR *pData, int len)
{
if( len <= )
return ; EnterCriticalSection(&m_cs);
while(IsOverFlowCondition(len))///判断缓冲区剩余空间是否够存放len长的数据
{
BufferResize(len);///若不够,则扩充缓冲区.
} if(IsIndexOverFlow(len))///判断"尾"指针的位置.
{
int FirstCopyLen = m_iBufSize-m_iTailPos;
int SecondCopyLen = len - FirstCopyLen;
CopyMemory(m_pBuffer+m_iTailPos, pData, FirstCopyLen);
if (SecondCopyLen)
{
CopyMemory(m_pBuffer, pData+FirstCopyLen, SecondCopyLen);
m_iTailPos = SecondCopyLen;
}
else
m_iTailPos = ;
}
else
{
CopyMemory(m_pBuffer+m_iTailPos, pData, len);
m_iTailPos += len;
} LeaveCriticalSection(&m_cs);
return ;
} void CCircularBufferPool::GetData(TCHAR *pData, int len, bool Delete)
{
if(len < m_iBufSize-m_iHeadPos)
{
CopyMemory(pData, m_pBuffer+m_iHeadPos, len);
if(Delete==true)
m_iHeadPos += len;
}
else
{
int fc, sc;
fc = m_iBufSize-m_iHeadPos;
sc = len - fc;
CopyMemory(pData, m_pBuffer+m_iHeadPos, fc);
if (sc)
CopyMemory(pData+fc, m_pBuffer, sc);
if(Delete==true)
m_iHeadPos = sc;
if(m_iHeadPos >= m_iBufSize)
m_iHeadPos = ;
}
} //
//进行自定义包的解析
//
int CCircularBufferPool::GetFullPacket( TCHAR *Buf, int &nLen )
{
EnterCriticalSection(&m_cs);
if(GetValidCount() < m_PacketHeadLen )//当前缓冲区不够一个包头的数据
{
LeaveCriticalSection(&m_cs);
return ;
} GetData(Buf,m_PacketHeadLen,false);
PACKAGE_HEAD *p = (PACKAGE_HEAD *)Buf;
if( (GetValidCount()-m_PacketHeadLen) < (int)p->nDataLen )//当前缓冲区不够一个包体的数据
{
LeaveCriticalSection(&m_cs);
return ;
} //判断包的合法性
int IsIntegrallity = ValidatePackIntegrality(p);
if( IsIntegrallity != )
{
LeaveCriticalSection(&m_cs);
return IsIntegrallity;
} GetData(Buf,m_PacketHeadLen+p->nDataLen,true);
nLen = m_PacketHeadLen+p->nDataLen; LeaveCriticalSection(&m_cs);
return ;
}

2、利用底层的缓冲区来进行拆包
  由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据,这样一来就不需要为每一个连接分配一个缓冲区了。另一方面我们知道 recv 或者 wsarecv 都有一个参数,用来表示我们要接收多长长度的数据。利用这两个条件我们就可以对第一种方法进行优化了。
      对于阻塞 SOCKET 来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据。
   相关代码如下:

char PackageHead[];
char PackageContext[*]; int len;
PACKAGE_HEAD *pPackageHead;
while( m_bClose == false )
{
memset(PackageHead,,sizeof(PACKAGE_HEAD));
len = m_TcpSock.ReceiveSize((char*)PackageHead,sizeof(PACKAGE_HEAD));
if( len == SOCKET_ERROR )
{
break;
}
if(len == )
{
break;
}
pPackageHead = (PACKAGE_HEAD *)PackageHead;
memset(PackageContext,,sizeof(PackageContext));
if(pPackageHead->nDataLen>)
{
len = m_TcpSock.ReceiveSize((char*)PackageContext,pPackageHead->nDataLen);
}
}

  

  其中,m_TcpSock 是一个封装了SOCKET的类的变量,其中的 ReceiveSize 用于接收一定长度的数据,直到接收了一定长度的数据或者网络出错才返回。

int winSocket::ReceiveSize( char* strData, int iLen )
{
if( strData == NULL )
return ERR_BADPARAM;
char *p = strData;
int len = iLen;
int ret = ;
int returnlen = ; while( len > )
{
ret = recv( m_hSocket, p+(iLen-len), iLen-returnlen, );
if ( ret == SOCKET_ERROR || ret == )
{
return ret;
} len -= ret;
return len += ret;
} return returnlen;

  对于非阻塞的 SOCKET,比如完成端口,我们可以提交接收包头长度的数据的请求,当 GetQueuedCompletionStatus 返回时,我们判断接收的数据长度是否等于包头长度,若等于,则提交接收包体长度的数据的请求,若不等于则提交接收剩余数据的请求。当接收包体时,采用类似的方法。
  下面给出相关代码:

enum IOType
{
IOInitialize,
IORead,
IOWrite,
IOIdle
}; class OVERLAPPEDPLUS
{
public:
OVERLAPPED m_ol;
IOType m_ioType;
bool m_bIsPackageHead;//当前接收的数据是否是包头数据。 int m_count;
WSABUF m_wsaBuffer;
int m_RecvPos;
char m_Buffer[*];//此缓冲要尽可能大 OVERLAPPEDPLUS(IOType ioType)
{
ZeroMemory(this, sizeof(OVERLAPPEDPLUS));
m_ioType = ioType;
}
};

接收连接后发出的第一个请求,请求接收包头大小的数据.

OVERLAPPEDPLUS *pOverlappedPlus =  new OVERLAPPEDPLUS;
pOverlappedPlus->m_wsaBuffer.buf = pOverlappedPlus->m_Buffer;
pOverlappedPlus->m_wsaBuffer.len = PACKAGE_HEAD_LEN;///包头的长度
pOverlappedPlus->m_bIsPackageHead = true;
pOverlappedPlus->m_RecvPos = ;
pOverlappedPlus->m_ioType = IORead; DWORD RecvBytes;
DWORD Flags; Flags = ;
if (WSARecv(clientSocket, &(pOverlappedPlus->m_wsaBuffer), , &RecvBytes, &Flags, &pOverlappedPlus->m_ol, NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() != ERROR_IO_PENDING)
{
delete pOverlappedPlus;
}
else
{
///相关的错误处理
}
}
else
{
///相关的错误处理
}

在GetQueuedCompletionStatus所在的函数中:

if( pOverlapPlus->m_ioType== IORead)
{
if( pOverlapPlus->m_wsaBuffer.len == dwIoSize )
{
if( pOverlapPlus->m_bIsPackageHead == true )///接收到的是包头。
{
PACKAGE_HEAD *pPackageHead = (PACKAGE_HEAD *)(pOverlapPlus->m_Buffer); if(pThis->IsLegalityPackageHead(pPackageHead)==false)///判断是否是合法的包
{
closesocket(lpClientContext->m_Socket);
continue;
} pOverlapPlus->m_bIsPackageHead = false;
pOverlapPlus->m_wsaBuffer.len = pPackageHead->nDataLen;
pOverlapPlus->m_RecvPos += dwIoSize;
pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos; }
else///接收到的是包体
{
pOverlapPlus->m_RecvPos += dwIoSize;
///这时pOverlapPlus->m_Buffer里就存放了一个完整的数据包,长度为pOverlapPlus->m_RecvPos ///继续请求 请求下一个数据包的包头
pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer;
memset(pOverlapPlus->m_Buffer,,sizeof(pOverlapPlus->m_Buffer));
pOverlapPlus->m_wsaBuffer.len = PACKAGE_HEAD_LEN;
pOverlapPlus->m_bIsPackageHead = true;
pOverlapPlus->m_RecvPos = ;
}
}
else///接收的数据还不完整
{
pOverlapPlus->m_wsaBuffer.len -= dwIoSize;
pOverlapPlus->m_RecvPos += dwIoSize;
pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos;
} pOverlapPlus->m_ioType = IORead;
state = WSARecv(lpClientContext->m_Socket, &(pOverlapPlus->m_wsaBuffer), , &RecvBytes, &Flags, &pOverlapPlus->m_ol, NULL);
if ( state == SOCKET_ERROR)
{
if(WSAGetLastError() != ERROR_IO_PENDING)
{
//关闭套接字 释放相应资源
continue;
}
}
}

三、如何判断包的合法性.

  判断包的合法性可以结合下面两种方式来判断.但是想100%的判定出非法包,只能通过信息安全中的知识来判定了,对这种方法这里不做阐述.
  1、通过包头的结构来判断包的合法性.
   最初的时候我是根据包头来判断包的合法性,比如判断Command是否超出命令范围,nDataLen是否大于最大包的长度.但是这种方法无法过滤掉非法包,当出现非法包时我们唯一能做的就是断开连接,或许这也是最好的处理办法.
   我们可以给一个完整的包加上开始和结束标志,标志可以是个整数,也可以是一串字符串.以第一种拆包方式为例来说明.当要拆一个完整包时我们先从缓冲区有效数据头指针地址搜索包的开始标志,搜索到后并且当前数据够一个包头数据,则判断开始标志和包头是否合法,若合法则根据代表数据长度的变量的值定位到包尾,判断包尾标志是否与我们定义的一致,若一致则这个包是合法的包.若有一项不一致则继续寻找下个包的开始标志,并把下个合法包的前面的数据全部舍弃.
  2、通过逻辑层来判断包的合法性.
  当取出一个合法的包时,我们还要根据当前数据处理的逻辑来判断包的合法性.比如说在登陆成功后的某段时间服务器又收到了同一个客户端的登陆包,那我们就可以判断这个包是非法的,简单处理就是断开连接.

TCP的封包与拆包的更多相关文章

  1. TCP数据包的封包和拆包

    //该段博文为引用,非原创. 封包和拆包 作者:fengge8ylf  博客:http://blog.csdn.net/fengge8ylf 对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就 ...

  2. TCP粘包和拆包问题

    问题产生 一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题. 下面可以看一张图,是客户端向服务端发送包: 1. 第一种情况 ...

  3. netty 解决TCP粘包与拆包问题(一)

    1.什么是TCP粘包与拆包 首先TCP是一个"流"协议,犹如河中水一样连成一片,没有严格的分界线.当我们在发送数据的时候就会出现多发送与少发送问题,也就是TCP粘包与拆包.得不到我 ...

  4. rtp的封包与拆包h264

    请看文档rfc3984 1.看h264的帧 SPS序列參数帧 00 00 00 01 67 64 . . .. PPS图像參数帧 00 00 00 01 68 EE... . I帧 00 00 00 ...

  5. C#下利用封包、拆包原理解决Socket粘包、半包问题(新手篇)

    介于网络上充斥着大量的含糊其辞的Socket初级教程,扰乱着新手的学习方向,我来扼要的教一下新手应该怎么合理的处理Socket这个玩意儿. 一般来说,教你C#下Socket编程的老师,很少会教你如何解 ...

  6. tcp粘包和拆包的处理方案

    随着智能硬件越来越流行,很多后端开发人员都有可能接触到socket编程.而很多情况下,服务器与端上需要保证数据的有序,稳定到达,自然而然就会选择基于tcp/ip协议的socekt开发.开发过程中,经常 ...

  7. 【Netty】TCP粘包和拆包

    一.前言 前面已经基本上讲解完了Netty的主要内容,现在来学习Netty中的一些可能存在的问题,如TCP粘包和拆包. 二.粘包和拆包 对于TCP协议而言,当底层发送消息和接受消息时,都需要考虑TCP ...

  8. TCP粘包,拆包及解决方法

    在进行Java NIO学习时,发现,如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题.我们都知道TCP属于传输 ...

  9. TCP粘包、拆包

    TCP粘包.拆包 熟悉tcp编程的可能都知道,无论是服务端还是客户端,当我们读取或发送数据的时候,都需要考虑TCP底层的粘包/拆包机制. TCP是一个“流”协议,所谓流就是没有界限的遗传数据.可以想象 ...

随机推荐

  1. apache-php安装mysql简单方法

    1.启用mysql功能,在php.ini中 extension=php_mysql.dll extension=php_mysqli.dll 2. 修改extension_dir = "ex ...

  2. javascript 设为首页 | 加入收藏夹 JS代码

    我们介绍一个可兼容所有浏览器的加入收藏代码代码,大概原理是这样的我们根据获取用户navigator.userAgent.toLowerCase()信息来判断浏览器,根据浏览器是否支持加入收藏js命令, ...

  3. MongoDB 复制集模式Replica Sets

    1.概述 复制集是一个带有故障转移的主从集群.是从现有的主从模式演变而来,增加了自动故障转移和节点成员自动恢复. 复制集模式中没有固定的主结点,在启动后,多个服务节点间将自动选举 产生一个主结点.该主 ...

  4. 解决nginx: [error] open() "/usr/local/nginx/logs/nginx.pid" failed错误

    重新启动服务器,访问web服务发现无法浏览,登陆服务器之 后进到nginx使用./nginx -s reload重新读取配置文件,发现报nginx: [error] open() "/usr ...

  5. 关于HTML编辑页面(1)

    1,<title>...</title> //省略号表示的是网页标题 2,<body>...</body>//省略号表示的是网页正文内容 3,在Drea ...

  6. 使用OpenSSL API进行安全编程

    http://www.ibm.com/developerworks/cn/linux/l-openssl.html OpenSSL API 的文档有些含糊不清.因为还没有多少关于 OpenSSL 使用 ...

  7. Google测试精华文章(1) - 测试行为,而非实现

    Your trusty Calculator class is one of your most popular open source projects, with many happy users ...

  8. linux主要目录的作用

    手动敲一遍.算是加强记忆吧~ /:文件系统的入口,也是最高一级的目录 /bin:最基本的且着急用户和普通用户都可以使用的命令放在此目录下,如:ls.cp等 /boot:存放Linux的内核及引导系统所 ...

  9. 初始化rails上的compass项目

    compass以外还有一个很实用的scss模块, _media-queries.scss 通过终端下载 curl -O https://raw.github.com/paranoida/sass-me ...

  10. Door man

    poj1300:http://poj.org/problem?id=1300 题意:给你n个房间,房间之间有一些门,房间是按0~~n-进行编号的.然后给出一些房间的之间门,n行,每行的数字表示该们与其 ...