【tcp socket的發送與接收緩沖區】
發送緩沖隊列
struct sk_buff
【TCP接收/發送滑動窗口與內核接收/發送緩沖區之間的關系】
滑動窗口的大小與套接字緩存區會在一定程度上影響並發連接的數據,每個TCP連接都會為維護TCP滑動窗口而消耗內存,這個窗口會根據服務器的處理速度收縮或擴張。
整個數據的流程中,首先網卡接收到的數據存放到內核緩沖區內,然后內核緩沖區存放的數據根據TCP信息將數據移動到具體的某一個TCP連接上的接收緩沖區內,也就是接收滑動窗口內,然后應用程序從TCP的接受緩沖區內讀取數據,如果應用程序一直不讀取,那么滑動窗口就會變小,直至為0.
如果網卡處理數據的速度比內核處理數據的速度慢,那么內核會有一個隊列來保存這些數據,這個隊列的大小就是由參數netdev_max_backlog決定的
對於發送數據來說,應用程序將數據拷貝到各自TCP發送緩沖區內(也就是發送滑動窗口),然后系統的所有TCP套接字上發送緩沖區(也就是發送滑動窗口)內的數據都將數據拷貝到內核發送緩沖區內,然后內核將內核緩沖區的數據經過網卡發送出去。
TCP的發送/接受緩沖區(也就是發送/接受滑動窗口),是針對某一個具體的TCP連接來說的,每一個TCP連接都會有相應的滑動窗口,但是內核的發送/接受緩沖區是針對整個系統的,里面存放着整個系統的所有TCP連接的接收/發送的數據。
每個TCP套接口有一個發送緩沖區,可以用SO_SNDBUF套接口選項來改變這一緩沖區的大小。當應用進程調用write往套接口寫數據時,內核從應用進程緩沖區中拷貝所有數據到套接口的發送緩沖區,如果套接口發送緩沖區容不下應用程序的所有數據,或者是應用進程的緩沖區大於套接口的發送緩沖區,或者是套接口的發送緩沖區中有別的數據,應用進程將被掛起。內核將不從write返回。直到應用進程緩沖區中的所有數據都拷貝到套接口發送緩沖區。所以,從寫一個TCP套接口的write調用成功返回僅僅表示我們可以重新使用應用進程緩沖區,它並不是告訴我們對方收到數據。
TCP發給對方的數據,對方在收到數據時必須給矛確認,只有在收到對方的確認時,本方TCP才會把TCP發送緩沖區中的數據刪除。
UDP因為是不可靠連接,不必保存應用進程的數據拷貝,應用進程中的數據在沿協議棧向下傳遞時,以某種形式拷貝到內核緩沖區,當數據鏈路層把數據傳出后就把內核緩沖區中數據拷貝刪除。因此它不需要一個發送緩沖區。
寫UDP套接口的write返回表示應用程序的數據或數據分片已經進入鏈路層的輸出隊列,如果輸出隊列沒有足夠的空間存放數據,將返回錯誤ENOBUFS.
應用程序可通過調用send(write, sendmsg等)利用tcp socket向網絡發送應用數據,而tcp/ip協議棧再通過網絡設備接口把已經組織成struct sk_buff的應用數據(tcp數據報)真正發送到網絡上,由於應用程序調用send的速度跟網絡介質發送數據的速度存在差異,所以,一部分應用數據被組織成tcp數據報之后,會緩存在tcp socket的發送緩存隊列中,等待網絡空閑時再發送出去。
同時,tcp協議要求對端在收到tcp數據報后,要對其序號進行ACK,只有當收到一個tcp 數據報的ACK之后,才可以把這個tcp數據報(以一個struct sk_buff的形式存在)從socket的發送緩沖隊列中清除。
tcp socket的發送緩沖區實際上是一個結構體struct sk_buff的隊列,我們可以把它稱為發送緩沖隊列,由結構體struct sock的成員sk_write_queue表示。sk_write_queue是一個結構體struct sk_buff_head類型,這是一個struct sk_buff的雙向鏈表,其定義如下:
struct sk_buff_head {
struct sk_buff *next; //后指針
struct sk_buff *prev; //前指針
__u32 qlen; //隊列長度(即含有幾個struct sk_buff)
spinlock_t lock; //鏈表鎖
};
內核代碼中,先在這個隊列中創建足夠存放數據的struct sk_buff,然后向隊列存入應用數據。
結構體struct sock的成員sk_wmem_queued表示發送緩沖隊列中已分配的字節數,一般來說,分配一個struct sk_buff是用於存放一個tcp數據報,其分配字節數應該是MSS+協議首部長度。在我的實驗環境中,MSS值是1448,協議首部取最大長度 MAX_TCP_HEADER,在我的實驗環境中為224。經數據對齊處理后,最后struct sk_buff的truesize為1956。也就是隊列中每分配一個struct sk_buff,成員sk_wmem_queue的值就增加1956。
struct sock的成員sk_forward_alloc是表示預分配長度。當我們第一次要為發送緩沖隊列分配一個struct sk_buff時,我們並不是直接分配需要的內存大小,而是會以內存頁為單位進行的預分配。
tcp協議分配struct sk_buff的函數是sk_stream_alloc_pskb。它首先根據傳入的參數指定的大小在內存中分配一個struct sk_buff,如果成功,sk_forward_alloc取該大小值,並向上取整到頁(4096字節)的整數倍。並累加到struct sock的成員sk_prot,也即表示tcp協議的結構體mytcp_prot的成員memory_allocated中,該成員是一個指針,指向變量 tcp_memory_allocated,它表示的是當前整個TCP協議當前為緩沖區所分配的內存(包括讀緩沖隊列)
當把這個新分配成功的struct sk_buff放入到緩沖隊列sk_write_queue后,從sk_forward_alloc中減去該sk_buff的truesize值。第二次分配struct sk_buff時,只要再從sk_forward_alloc中減去新的sk_buff的truesize即可,如果sk_forward_alloc已經小於當前的truesize,則將其再加上一個頁的整數倍值,並累加入tcp_memory_allocated。
也就是說,通過sk_forward_alloc使全局變量tcp_memory_allocated保存當前tcp協議總的緩沖區分配內存的大小,並且該大小是頁邊界對齊的。
(2)
前面講到struct sock的成員sk_forward_alloc表示預分配內存大小,用於向全局變量mytcp_memory_allocated累加當前已分配的整個 TCP協議的緩沖區大小。之所以要累加這個值,是為了對tcp協議總的可用緩沖區大小作限制。表示TCP協議的結構體mytcp_prot還有幾個成員與緩沖區相關。
mysysctl_tcp_mem是一個數組,由mytcp_prot的成員sysctl_mem指向,數組共有三個元素,mysysctl_tcp_mem[0]表示對緩沖區總的可用大小的最低限制,當前總共分配的緩沖區大小低於這個值,則沒有問題,分配成功。 mysysctl_tcp_mem[2]表示對緩沖區可用大小的最高硬性限制,一旦總分配的緩沖區大小超出這個值,我們只好把tcp
socket 的發送緩沖區的預設大小sk_sndbuf減小為已分配緩沖隊列大小的一半,但不能小於SOCK_MIN_SNDBUF(2K),但保證這一次的分配成功。mysysctl_tcp_mem[1]介於前面兩個值的中間,這是一個警告值,一旦超出這個值,進入警告狀態,這個狀態下,根據調用參數來決定此次分配是否成功。
這三個值的大小是根據所在系統的內存大小,在初始化時決定的,在我的實驗環境中,內存大小為256M,這三個值分配是:96K,128K,192K。它們可以通過/proc文件系統,在/proc/sys/net/ipv4/tcp_mem中進行修改。當然,除非特別需要,一般無需改動這些缺省值。
mysysctl_tcp_wmem也是一個同樣結構的數組,表示發送緩沖區的大小限制,由mytcp_prot的成員sysctl_wmem指向,其缺省值分別是4K,16K,128K。可以通過/proc文件系統,在/proc/sys/net/ipv4/tcp_wmem中進行修改。struct sock的成員sk_sndbuf的值是真正的發送緩沖隊列的預設大小,其初始值取中間一個16K。在tcp數據報的發送過程中,一旦 sk_wmem_queued超過sk_sndbuf的值,則發送停止,等待發送緩沖區可用。因為有可能一批已發送出去的數據還沒有收到ACK,同時,緩沖隊列中的數據也可全部發出去,已達到清空緩沖隊列的目的,所以,只要在網絡不是很差的情況下(差到沒有辦法收到ACK),這個等待在一段時間后會成功的。
全局變量mytcp_memory_pressure是一個標志,在tcp緩沖大小進入警告狀態時,它置1,否則置0。
(3)
mytcp_sockets_allocated是到目前為止,整個tcp協議中創建的socket的個數,由mytcp_prot的成員 sockets_allocated指向。可以在/proc/net/sockstat文件中查看,這只是一個供統計查看用的數據,沒有任何實際的限制作用。
mytcp_orphan_count表示整個tcp協議中待銷毀的socket的個數(已無用的socket),由mytcp_prot的成員orphan_count指向,也可以在/proc/net/sockstat文件中查看。
mysysctl_tcp_rmem是跟mysysctl_tcp_wmem相同結構的數組,表示接收緩沖區的大小限制,由mytcp_prot的成員 sysctl_rmem指向,其缺省值分別是4096bytes,87380bytes,174760bytes。它們可以通過/proc文件系統,在 /proc/sys/net/ipv4/tcp_rmem中進行修改。struct sock的成員sk_rcvbuf表示接收緩沖隊列的大小,其初始值取mysysctl_tcp_rmem[1],成員sk_receive_queue 是接收緩沖隊列,結構跟sk_write_queue相同。
tcp socket的發送緩沖隊列跟接收緩沖隊列的大小既可以通過/proc文件系統進行修改,也可以通過TCP選項操作進行修改。套接字級別上的選項 SO_RCVBUF可用於獲取和修改接收緩沖隊列的大小(即strcut sock->sk_rcvbuf的值),比如下列的代碼可用於獲取當前系統的接收緩沖隊列大小:
int rcvbuf_len;
int len = sizeof(rcvbuf_len);
if( getsockopt( fd, SOL_SOCKET, SO_RCVBUF, (void *)&rcvbuf_len, &len ) < 0 ){
perror("getsockopt: ");
return -1;
}
printf("the recevice buf len: %d\n", rcvbuf_len );
而套接字級別上的選項SO_SNDBUF則用於獲取和修改發送緩沖隊列的大小(即struct sock->sk_sndbuf的值),代碼同上,只需改SO_RCVBUF為SO_SNDBUF即可。
獲取發送和接收緩沖區的大小相對簡單一些,而設置的操作在內核中動作會稍微復雜一些,另外,在接口上也會有所差異,即由setsockopt傳入的表示緩沖區大小的參數是實際大小的1/2,即,如果想要設發送緩沖區的大小為20K,則需要這樣調用setsockopt:
int rcvbuf_len = 10 * 1024; //實際緩沖區大小的一半。
int len = sizeof(rcvbuf_len);
if( setsockopt( fd, SOL_SOCKET, SO_SNDBUF, (void *)&rcvbuf_len, len ) < 0 ){
perror("getsockopt: ");
return -1;
}
在內核中,首先內核要判斷新設置的值是否超過上限,若超過,則取上限為新值,發送和接收緩沖區大小的上限值分別為sysctl_wmem_max和 sysctl_rmem_max的2倍。這兩個全局變量的值是相等的,都為(sizeof(struct sk_buff) + 256) * 256,大概為64K負載數據,由於struct sk_buff的影響,實際發送和接收緩沖區的大小最大都可設到210K左右。它們的下限是2K,即緩沖區大小不能低於2K。
另外,SO_SNDBUF和SO_RCVBUF有一個特殊的版本:SO_SNDBUFFORCE和SO_RCVBUFFORCE,它們不受發送和接收緩沖區大小上限的限制,可設置不小於2K的任意緩沖區大小。(完)
===================================================
以下內容是Socket相關參數的設置方法
1. 如果在已經處於 ESTABLISHED狀態下的socket(一般由端口號和標志符區分)調用
closesocket(一般不會立即關閉而經歷TIME_WAIT的過程)后想繼續重用該socket:
BOOL bReuseaddr=TRUE;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)&bReuseaddr,sizeof(BOOL));
2. 如果要已經處於連接狀態的soket在調用closesocket后強制關閉,不經歷
TIME_WAIT的過程:
BOOL bDontLinger = FALSE;
setsockopt(s,SOL_SOCKET,SO_DONTLINGER,(const char*)&bDontLinger,sizeof(BOOL));
3.在send(),recv()過程中有時由於網絡狀況等原因,發收不能預期進行,而設置收發時限:
int nNetTimeout=1000;//1秒
//發送時限
setsockopt(socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
//接收時限
setsockopt(socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));
4.在send()的時候,返回的是實際發送出去的字節(同步)或發送到socket緩沖區的字節
(異步);系統默認的狀態發送和接收一次為8688字節(約為8.5K);在實際的過程中發送數據
和接收數據量比較大,可以設置socket緩沖區,而避免了send(),recv()不斷的循環收發:
// 接收緩沖區
int nRecvBuf=32*1024;//設置為32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
//發送緩沖區
int nSendBuf=32*1024;//設置為32K
setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
5. 如果在發送數據的時,希望不經歷由系統緩沖區到socket緩沖區的拷貝而影響
程序的性能:
int nZero=0;
setsockopt(socket,SOL_S0CKET,SO_SNDBUF,(char *)&nZero,sizeof(nZero));
6.同上在recv()完成上述功能(默認情況是將socket緩沖區的內容拷貝到系統緩沖區):
int nZero=0;
setsockopt(socket,SOL_S0CKET,SO_RCVBUF,(char *)&nZero,sizeof(int));
7.一般在發送UDP數據報的時候,希望該socket發送的數據具有廣播特性:
BOOL bBroadcast=TRUE;
setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(BOOL));
8.在client連接服務器過程中,如果處於非阻塞模式下的socket在connect()的過程中可
以設置connect()延時,直到accpet()被呼叫(本函數設置只有在非阻塞的過程中有顯著的
作用,在阻塞的函數調用中作用不大)
BOOL bConditionalAccept=TRUE;
setsockopt(s,SOL_SOCKET,SO_CONDITIONAL_ACCEPT,(const char*)&bConditionalAccept,sizeof(BOOL));
9.如果在發送數據的過程中(send()沒有完成,還有數據沒發送)而調用了closesocket(),以前我們
一般采取的措施是"從容關閉"shutdown(s,SD_BOTH),但是數據是肯定丟失了,如何設置讓程序滿足具體
應用的要求(即讓沒發完的數據發送出去后在關閉socket)?
struct linger {
u_short l_onoff;
u_short l_linger;
};
linger m_sLinger;
m_sLinger.l_onoff=1;//(在closesocket()調用,但是還有數據沒發送完畢的時候容許逗留)
// 如果m_sLinger.l_onoff=0;則功能和2.)作用相同;
m_sLinger.l_linger=5;//(容許逗留的時間為5秒)
setsockopt(s,SOL_SOCKET,SO_LINGER,(const char*)&m_sLinger,sizeof(linger));
Note:1.在設置了逗留延時,用於一個非阻塞的socket是作用不大的,最好不用;
2.如果想要程序不經歷SO_LINGER需要設置SO_DONTLINGER,或者設置l_onoff=0;
10.還一個用的比較少的是在SDI或者是Dialog的程序中,可以記錄socket的調試信息:
(前不久做過這個函數的測試,調式信息可以保存,包括socket建立時候的參數,采用的
具體協議,以及出錯的代碼都可以記錄下來)
BOOL bDebug=TRUE;
setsockopt(s,SOL_SOCKET,SO_DEBUG,(const char*)&bDebug,sizeof(BOOL));
11.附加:往往通過setsockopt()設置了緩沖區大小,但還不能滿足數據的傳輸需求,
我的習慣是自己寫個處理網絡緩沖的類,動態分配內存;下面我將這個類寫出,希望對
初學者有所幫助:
//仿照String 改寫而成
//==============================================================================
// 二進制數據,主要用於收發網絡緩沖區的數據
// CNetIOBuffer 以 MFC 類 CString 的源代碼作為藍本改寫而成,用法與 CString 類似,
// 但是 CNetIOBuffer 中存放的是純粹的二進制數據,'/0' 並不作為它的結束標志。
// 其數據長度可以通過 GetLength() 獲得,緩沖區地址可以通過運算符 LPBYTE 獲得。
//==============================================================================
// Copyright (c) All-Vision Corporation. All rights reserved.
// Module: NetObject
// File: SimpleIOBuffer.h
// Author: gdy119
// Email : 8751webmaster@126.com
// Date: 2004.11.26
//==============================================================================
// NetIOBuffer.h
#ifndef _NETIOBUFFER_H
#define _NETIOBUFFER_H
//=============================================================================
#define MAX_BUFFER_LENGTH 1024*1024
//=============================================================================
//主要用來處理網絡緩沖的數據
class CNetIOBuffer
{
protected:
LPBYTE m_pbinData;
int m_nLength;
int m_nTotalLength;
CRITICAL_SECTIONm_cs;
void Initvalibers();
public:
CNetIOBuffer();
CNetIOBuffer(const LPBYTE lbbyte, int nLength);
CNetIOBuffer(const CNetIOBuffer&binarySrc);
virtual ~CNetIOBuffer();
//=============================================================================
BOOL CopyData(const LPBYTE lbbyte, int nLength);
BOOL ConcatData(const LPBYTE lbbyte, int nLength);
void ResetIoBuffer();
int GetLength() const;
BOOL SetLength(int nLen);
LPBYTE GetCurPos();
int GetRemainLen();
BOOL IsEmpty() const;
operator LPBYTE() const;
static GetMaxLength() { return MAX_BUFFER_LENGTH; }
const CNetIOBuffer& operator=(const CNetIOBuffer& buffSrc);
};
#endif //
// NetOBuffer.cpp: implementation of the CNetIOBuffer class.
//======================================================================
#include "stdafx.h"
#include "NetIOBuffer.h"
//======================================================================
//=======================================================================
// Construction/Destruction
CNetIOBuffer::CNetIOBuffer()
{
Initvalibers();
}
CNetIOBuffer::CNetIOBuffer(const LPBYTE lbbyte, int nLength)
{
Initvalibers();
CopyData(lbbyte, nLength);
}
CNetIOBuffer::~CNetIOBuffer()
{
delete []m_pbinData;
m_pbinData=NULL;
DeleteCriticalSection(&m_cs);
}
CNetIOBuffer::CNetIOBuffer(const CNetIOBuffer&binarySrc)
{
Initvalibers();
CopyData(binarySrc,binarySrc.GetLength());
}
void CNetIOBuffer::Initvalibers()
{
m_pbinData = NULL;
m_nLength = 0;
m_nTotalLength = MAX_BUFFER_LENGTH;
if(m_pbinData==NULL)
{
m_pbinData=new BYTE[m_nTotalLength];
ASSERT(m_pbinData!=NULL);
}
InitializeCriticalSection(&m_cs);
}
void CNetIOBuffer::ResetIoBuffer()
{
EnterCriticalSection(&m_cs);
m_nLength = 0;
memset(m_pbinData,0,m_nTotalLength);
LeaveCriticalSection(&m_cs);
}
BOOL CNetIOBuffer::CopyData(const LPBYTE lbbyte, int nLength)
{
if( nLength > MAX_BUFFER_LENGTH )
return FALSE;
ResetIoBuffer();
EnterCriticalSection(&m_cs);
memcpy(m_pbinData, lbbyte, nLength );
m_nLength = nLength;
LeaveCriticalSection(&m_cs);
return TRUE;
}
BOOL CNetIOBuffer::ConcatData(const LPBYTE lbbyte, int nLength)
{
if( m_nLength + nLength > MAX_BUFFER_LENGTH )
return FALSE;
EnterCriticalSection(&m_cs);
memcpy(m_pbinData+m_nLength, lbbyte, nLength );
m_nLength += nLength;
LeaveCriticalSection(&m_cs);
return TRUE;
}
int CNetIOBuffer::GetLength() const
{
return m_nLength;
}
BOOL CNetIOBuffer::SetLength(int nLen)
{
if( nLen > MAX_BUFFER_LENGTH )
return FALSE;
EnterCriticalSection(&m_cs);
m_nLength = nLen;
LeaveCriticalSection(&m_cs);
return TRUE;
}
LPBYTE CNetIOBuffer::GetCurPos()
{
if( m_nLength < MAX_BUFFER_LENGTH )
return (m_pbinData+m_nLength);
else
return NULL;
}
CNetIOBuffer:: operator LPBYTE() const
{
return m_pbinData;
}
int CNetIOBuffer::GetRemainLen()
{
return MAX_BUFFER_LENGTH - m_nLength;
}
BOOL CNetIOBuffer::IsEmpty() const
{
return m_nLength == 0;
}
const CNetIOBuffer& CNetIOBuffer:: operator=(const CNetIOBuffer& buffSrc)
{
if(&buffSrc!=this)
{
CopyData(buffSrc, buffSrc.GetLength());
}
return *this;
}
回復人: PiggyXP(【小豬】●至愛VC,至愛網絡版●) ( ) 信譽:204
其實我覺得第5條很應該值得注意
int nZero=0;
setsockopt(socket,SOL_S0CKET,SO_SNDBUF,(char *)&nZero,sizeof(nZero));
記得以前有些朋友討論過,socket雖然send成功了,但是其實只是發送到數據緩沖區里面了,而並沒有真正的在物理設備上發送出去;而通過這條語句,將發送緩沖區設置為0,即屏蔽掉發送緩沖以后,一旦send返回(當然是就阻塞套結字來說),就可以肯定數據已經在發送的途中了^_^,但是這樣做也許會影響系統的性能
to:Sander()
UDP也有拷貝過程,但是UDP包有最大限制為64K;
TCP_NODELAY 一般用在the normal data stream 上;
12.發送數據時候一般是系統緩沖區滿以后才發送,現在設置為只要系統
緩沖區有數據就立刻發送:
BOOL bNodelay=TRUE;
SetSockOpt(s,IPPROTO_TCP,TCP_NODELAY,(const char*)&bNodelayt,sizeof(BOOL));