2005年09月14日

 

The first is the most extreme. You handle the WM_PAINT messages and do all the painting yourself. You get no help at all from Windows with this method. You have to create a device context, determine where and how big your control is, what state it is in, and then draw it all yourself. Most programmers tend to shy away from this.

Owner-draw (or self-draw) controls are a little easier. Here, Windows sets up the device context for you. It also fills in a structure that gives you the rectangle that the control occupies, the state the control is in, and flags to say how much drawing you need to do. You still need to do all the drawing yourself, but with all this information handed to you, it is not such a chore. In particular, for controls like list boxes and ListView controls, Windows will go line by line through the control and ask you to draw each individual item; you don’t need to know how to draw the entire control. To make things easy for owner-draw control writers, Windows also provides special drawing functions, like DrawFrameControl. Owner-draw is available for static, button, combo box and list box controls, but not for edit controls. It is also available for the ListView and Tab common controls. However, for ListView controls, owner draw only works in report mode and you have to draw the entire item (including all the sub items) in one go.

For Windows standard controls (buttons, edit boxes etc), one can also use the WM_CTLCOLOR family of notification messages to make some simple changes, usually just to the colours used. Windows sends WM_CTLCOLOR messages during the actually painting cycle of the control to give you the chance to make changes to the device context and nominate a brush to use for the background. You are somewhat limited in what you can do here, but the big advantage is that the control still does most of the work itself; you do not need to tell it how to draw itself. However, these messages are only available for the standard Windows controls; the Window Common controls do not support WM_CTLCOLOR.

Custom Draw

Windows common controls add yet another method of customising called custom draw. This is different to owner draw, although some programmers confuse the two. It is available for ListView, TreeView, ToolTip, Header, TrackBar, Toolbar and Rebar controls.

Custom draw lets you hook into the paint cycle of the control in one or more stages of the drawing process. At each draw stage you can make changes to the drawing context and choose to either do your own drawing, let the control do the drawing, or combine the two. You also get to control what further notifications you will receive during the paint cycle.

NM_CUSTOMDRAW Notifications

When the control first starts to paint itself, in response to a WM_PAINT, you receive a NM_CUSTOMDRAW notification message, with the draw stage set to CDDS_PREPAINT. If you don’t handle this yourself that will be the end of it, as the default message handler will just tell the control to carry on with default drawing and not to interrupt you again.

If you do handle the message, you have a chance to do a bit of painting yourself if you want. You can then set a flag in the return result that says whether you want the control to do its default painting; you usually will. You can also set flags to say whether you want to receive further notifications. You can ask the control to send you a WM_CUSTOMDRAW notification for the CDDS_POSTPAINT draw stage when the control has finished drawing (you can then do some extra drawing yourself). You can also say that you want to get notifications for the CDDS_ITEMPREPAINT draw stage for each item drawn.

Similarly, when you get each WM_CUSTOMDRAW notification for the CDDS_ITEMPREPAINT draw stage, you can set up the colours to use, make changes to the device context including font changes, and maybe do some drawing yourself. You can then say whether you want the default painting for the item and whether you want to receive a CDDS_ITEMPOSTPAINT when the item drawing is finished.

If you are in report mode, you can also ask for individual notifications for each subitem. These have the draw stage set to (CDDS_ITEMPREPAINT | CDDS_SUBITEM), and again you can fiddle with the device context, do your own drawing and/or let the control do some drawing and optionally receive a (CDDS_ITEMPOSTPAINT | CDDS_SUBITEM) drawing stage message at the end of each subitem.

NOTE: subitem notifications are only available for custom controls V4.71 (that is, for IE4.0 or Windows 98) or later, so please ensure that you check the version of COMCTL32 you have on your machine if you intend to use custom draw controls.

Here is a diagram showing the general flow of NM_CUSTOMDRAW notification messages for a list control with two items, each with two sub-items (columns): 

Why Custom Draw

OK. That’s a lot of notification messages. But what’s in it for me? Well, lets compare the advatnages of custom draw over owner draw for ListView controls:

Owner Draw Custom Draw
Only works with report-view style Works for any style
Must do all the drawing yourself Can choose you own drawing, default drawing of combinations of both. You can also change colours and fonts for default drawing.
Must draw the entire line (including subitems) in one go Can handle sub items individually

The price one pays for this flexibility is some complexity in writing the handler for the NM_CUSTOMDRAW. All the various drawing stages come through the one handler. It has to understand how to behave for each of the draw stages, extract the required information from the NMLVCUSTOMDRAW struct, and set the correct flags so that it receives subsequent notification messages correctly.

But things are more complicated than this. Some of the information in the NMLVCUSTOMDRAW struct is only relevant during certain draw-stage notifications. For example, Windows only sets the iSubItem value for the sub items notifications; it has rubbish values for the other notifications message. Similarly, Windows only fills in the RECT in the struct with valid values for some draw stages (depending on the version of common controls you have). Experimental evidence shows that even this is not completely correct; in fact only some of the values are valid during some drawing stages and others are not.

To ease the burden on the working class programmer, I have written a class called CListCtrlWithCustomDraw that does a lot of the housekeeping work for you, and uses simple virtual functions that you can override to provide the functionality you require. You do not need to worry about setting the correct flags, or unpacking information from structures and so on. In my next article, I’ll present not only this example code, but I’ll also walk you through the specifics on how exactly it all works.

The next step is to add a handler for the NM_CUSTOMDRAW notification message. Usually, to add a handler, one can simply right-click on CListCtrlWithCustomDraw in the class view, or use the WizardBar, and "Add windows message handler." However, this time there is a catch. NM_CUSTOMDRAW is nowhere to be seen in the list of available messages.

Well, it looks like the wizard is not going to do the job this time. However, we can get it to help. I find the easiest way to add a handler is to pick a similar message and then edit the resultant code. Even though the Wizard did not know about the message in the first place, it will quite happily work with the modified handler that results. In this case, I used the NM_OUTOFMEMORY. However, in order to reduce the amount of editing needed, I changed the name of the handler function to "OnCustomdraw". 

Now you can dive into the generated source code and manually edit the message map in the source file and change:

ON_NOTIFY_REFLECT(NM_OUTOFMEMORY, OnCustomdraw)

to read:

ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnCustomdraw)

When you look at the resulting OnCustomdraw function there are two arguments: pNMHDR and pResult. pResult is where we will set the flags that indicate when we want further custom draw messages and whether or not to use the default painting. For a List View control, the pNMHDR is actually a pointer to a NMLVCUSTOMDRAW structure (notification message for ListView custom draw) that tells us about the current drawing stage, what item or subitems we are looking at, and so forth.

We could edit the function declaration so it passes a NMLVCUSTOMDRAW*, but instead we can safely cast the pNMHDR from NMHDR* to NMLVCUSTOMDRAW* to give access to the data, even though these are distinct and separate structures.

As an aside, those from a C++ background may ask why NMLVCUSTOMDRAW does not simply derive from NMHDR in the first place. Well, that sort of thing just does not happen in the Windows SDK API. Because Microsoft designed the SDK API to work with both C and C++, it cannot use any C++ specific language features. That means all the structures in the SDK are PODs (plain old data structures) with no member functions, inheritance, and so on.

But all is not lost.

To get a similar effect to inheritance the SDK uses a C technique; the first member of a ‘derived’ struct is an instance of the ‘base’ struct. In this particular case, we have (with simplified declarations):

typedef struct {
 HWND hwndFrom;
 UINT idFrom;
 UINT code;
} NMHDR;

typedef struct {
 NMHDR  hdr;
 DWORD  dwDrawStage;
} NMCUSTOMDRAW;

typedef struct {
 NMCUSTOMDRAW nmcd;
 COLORREF clrText;
} NMLVCUSTOMDRAW;

Because NMLVCUSTOMDRAW is a POD, a pointer to a NMLVCUSTOMDRAW is also a pointer to its first member (nmcd), in other words a pointer to a NMCUSTOMDRAW. This in turn is a pointer to the first member of NMCUSTOMDRAW (hdr), which is a NMHDR. Therefore, it is quite legitimate to pass a pointer to a NMCUSTOMDRAW structure using a pointer to a NMHDR and cast it as required.

Now, on with the show.

The CListCtrlWithCustomDraw Class

In the OnCustomdraw for CListCtrlWithCustomDraw, I provide a generic handler for custom draw notifications. The bulk of the code is a switch statement on the draw stage we are up to (refer to my previous article). At each of the pre-paint stages, I call virtual functions to get color information, fonts required, and to take over (or augment) the drawing process. I also allow for extra drawing during the post-paint stages.

Let’s look at the fleshed out OnCustomdraw function for CListCtrlWithCustomDraw:

void CListCtrlWithCustomDraw::OnCustomdraw(NMHDR* pNMHDR,
                                           LRESULT* pResult)
{
 // first, lets extract data from
 // the message for ease of use later
 NMLVCUSTOMDRAW* pNMLVCUSTOMDRAW = (NMLVCUSTOMDRAW*)pNMHDR;

 // we'll copy the device context into hdc
 // but won't convert it to a pDC* until (and if)
 // we need it as this requires a bit of work
 // internally for MFC to create temporary CDC
 // objects
 HDC hdc = pNMLVCUSTOMDRAW->nmcd.hdc;
 CDC* pDC = NULL;

 // here is the item info
 // note that we don't get the subitem
 // number here, as this may not be
 // valid data except when we are
 // handling a sub item notification
 // so we'll do that separately in
 // the appropriate case statements
 // below.
 int nItem = pNMLVCUSTOMDRAW->nmcd.dwItemSpec;
 UINT nState = pNMLVCUSTOMDRAW->nmcd.uItemState;
 LPARAM lParam = pNMLVCUSTOMDRAW->nmcd.lItemlParam;

 // next we set up flags that will control
 // the return value for *pResult
 bool bNotifyPostPaint = false;
 bool bNotifyItemDraw = false;
 bool bNotifySubItemDraw = false;
 bool bSkipDefault = false;
 bool bNewFont = false;

 // what we do next depends on the
 // drawing stage we are processing
 switch (pNMLVCUSTOMDRAW->nmcd.dwDrawStage) {
  case CDDS_PREPAINT:
  {
   // PrePaint
   m_pOldItemFont = NULL;
   m_pOldSubItemFont = NULL;

   bNotifyPostPaint = IsNotifyPostPaint();
   bNotifyItemDraw = IsNotifyItemDraw();

   // do we want to draw the control ourselves?
   if (IsDraw()) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     CRect r(pNMLVCUSTOMDRAW->nmcd.rc);

    // do the drawing
    if (OnDraw(pDC,r)) {
     // we drew it all ourselves
     // so don't do default
     bSkipDefault = true;
    }
   }
  }
  break;

  case CDDS_ITEMPREPAINT:
  {
   // Item PrePaint
   m_pOldItemFont = NULL;

   bNotifyPostPaint = IsNotifyItemPostPaint(nItem,nState,lParam);
   bNotifySubItemDraw = IsNotifySubItemDraw(nItem,nState,lParam);

   // set up the colors to use
   pNMLVCUSTOMDRAW->clrText =
    TextColorForItem(nItem,nState,lParam);

   pNMLVCUSTOMDRAW->clrTextBk =
    BkColorForItem(nItem,nState,lParam);

   // set up a different font to use, if any
   CFont* pNewFont = FontForItem(nItem,nState,lParam);
   if (pNewFont) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     m_pOldItemFont = pDC->SelectObject(pNewFont);

    bNotifyPostPaint = true; // need to restore font
   }

   // do we want to draw the item ourselves?
   if (IsItemDraw(nItem,nState,lParam)) {
    if (! pDC) pDC = CDC::FromHandle(hdc);

    if (OnItemDraw(pDC,nItem,nState,lParam)) {
     // we drew it all ourselves
     // so don't do default
     bSkipDefault = true;
    }
   }
  }
  break;

  case CDDS_ITEMPREPAINT|CDDS_SUBITEM:
  {
   // Sub Item PrePaint
   // set sub item number (data will be valid now)
   int nSubItem = pNMLVCUSTOMDRAW->iSubItem;

   m_pOldSubItemFont = NULL;

   bNotifyPostPaint =
    IsNotifySubItemPostPaint(nItem, nSubItem, nState, lParam);

   // set up the colors to use
   pNMLVCUSTOMDRAW->clrText =
    TextColorForSubItem(nItem,nSubItem,nState,lParam);

   pNMLVCUSTOMDRAW->clrTextBk =
    BkColorForSubItem(nItem,nSubItem,nState,lParam);

   // set up a different font to use, if any
   CFont* pNewFont =
    FontForSubItem(nItem, nSubItem, nState, lParam);

   if (pNewFont) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     m_pOldSubItemFont = pDC->SelectObject(pNewFont);

     bNotifyPostPaint = true;    // need to restore font
    }

    // do we want to draw the item ourselves?
    if (IsSubItemDraw(nItem,nSubItem,nState,lParam)) {
     if (! pDC) pDC = CDC::FromHandle(hdc);
      if (OnSubItemDraw(pDC,nItem,nSubItem,nState,lParam)) {

     // we drew it all ourselves
     // so don't do default
     bSkipDefault = true;
    }
   }
  }
  break;

  case CDDS_ITEMPOSTPAINT|CDDS_SUBITEM:
  {
   // Sub Item PostPaint
   // set sub item number (data will be valid now)
   int nSubItem = pNMLVCUSTOMDRAW->iSubItem;

   // restore old font if any
   if (m_pOldSubItemFont) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     pDC->SelectObject(m_pOldSubItemFont);

    m_pOldSubItemFont = NULL;
   }

   // do we want to do any extra drawing?
   if (IsSubItemPostDraw()) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
    OnSubItemPostDraw(pDC,nItem,nSubItem,nState,lParam);
   }
  }
  break;

  case CDDS_ITEMPOSTPAINT:
  {
   // Item PostPaint
   // restore old font if any
   if (m_pOldItemFont) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
    pDC->SelectObject(m_pOldItemFont);
    m_pOldItemFont = NULL;
   }

   // do we want to do any extra drawing?
   if (IsItemPostDraw()) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     OnItemPostDraw(pDC,nItem,nState,lParam);
   }
  }
  break;

  case CDDS_POSTPAINT:
  {
   // Item PostPaint
   // do we want to do any extra drawing?
   if (IsPostDraw()) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     CRect r(pNMLVCUSTOMDRAW->nmcd.rc);

    OnPostDraw(pDC,r);
   }
  }
  break;
 }

 ASSERT(CDRF_DODEFAULT==0);
 *pResult = 0;
 if (bNotifyPostPaint) {
  *pResult |= CDRF_NOTIFYPOSTPAINT;
 }

 if (bNotifyItemDraw) {
  *pResult |= CDRF_NOTIFYITEMDRAW;
 }

 if (bNotifySubItemDraw) {
  *pResult |= CDRF_NOTIFYSUBITEMDRAW;
 }

 if (bNewFont) {
  *pResult |= CDRF_NEWFONT;
 }

 if (bSkipDefault) {
  *pResult |= CDRF_SKIPDEFAULT;
 }

 if (*pResult == 0) {
  // redundant as CDRF_DODEFAULT==0 anyway
  // but shouldn't depend on this in our code
  *pResult = CDRF_DODEFAULT;
 }
}

Phew! That’s a fair bit of code. The good news? There’s less code left for us to write when we derive from CListCtrlWithCustomDraw.

To make this work, those virtual functions need to be defined. The defaults for these functions are to do nothing; so using the CListCtrlWithCustomDraw on its own will work the same as a standard list control. It is only when you derive from this class and override some of the virtual functions that the ListView control changes appearance. If you do not override them, then you get the standard behaviour.

To start with, here are the additions to the class declaration for CListCtrlWithCustomDraw:

protected:
 CFont* m_pOldItemFont;
 CFont* m_pOldSubItemFont;

 //
 // Callbacks for whole control
 //

 // do we want to do the drawing ourselves?
 virtual bool IsDraw() { return false; }

 // if we are doing the drawing ourselves
 // override and put the code in here
 // and return TRUE if we did indeed do
 // all the drawing ourselves
 virtual bool OnDraw(CDC* /*pDC*/, const CRect& /*r*/)
 { return false; }

 // do we want to handle custom draw for
 // individual items
 virtual bool IsNotifyItemDraw() { return false; }

 // do we want to be notified when the
 // painting has finished
 virtual bool IsNotifyPostPaint() { return false; }

 // do we want to do any drawing after
 // the list control is finished
 virtual bool IsPostDraw() { return false; }

 // if we are doing the drawing afterwards ourselves
 // override and put the code in here
 // the return value is not used here
 virtual bool OnPostDraw(CDC* /*pDC*/, const CRect& /*r*/)
 { return false; }

 //
 // Callbacks for each item
 //

 // return a pointer to the font to use for this item.
 // return NULL to use default
 virtual CFont* FontForItem(int /*nItem*/,
                            UINT /*nState*/,
                            LPARAM /*lParam*/)
 { return NULL; }

 // return the text color to use for this item
 // return CLR_DEFAULT to use default
 virtual COLORREF TextColorForItem(int /*nItem*/,
                                   UINT /*nState*/,
                                   LPARAM /*lParam*/)
 { return CLR_DEFAULT; }

 // return the background color to use for this item
 // return CLR_DEFAULT to use default
 virtual COLORREF BkColorForItem(int /*nItem*/,
                                 UINT /*nState*/,
                                 LPARAM /*lParam*/)
 { return CLR_DEFAULT; }

 // do we want to do the drawing for this item ourselves?
 virtual bool IsItemDraw(int /*nItem*/,
                         UINT /*nState*/,
                         LPARAM /*lParam*/)
 { return false; }

 // if we are doing the drawing ourselves
 // override and put the code in here
 // and return TRUE if we did indeed do
 // all the drawing ourselves
 virtual bool OnItemDraw(CDC* /*pDC*/,
                         int /*nItem*/,
                         UINT /*nState*/,
                         LPARAM /*lParam*/)
 { return false; }

 // do we want to handle custom draw for
 // individual sub items
 virtual bool IsNotifySubItemDraw(int /*nItem*/,
                                  UINT /*nState*/,
                                  LPARAM /*lParam*/)
 { return false; }

 // do we want to be notified when the
 // painting has finished
 virtual bool IsNotifyItemPostPaint(int /*nItem*/,
                                    UINT /*nState*/,
                                    LPARAM /*lParam*/)
 { return false; }

 // do we want to do any drawing after
 // the list control is finished
 virtual bool IsItemPostDraw() { return false; }

 // if we are doing the drawing afterwards ourselves
 // override and put the code in here
 // the return value is not used here
 virtual bool OnItemPostDraw(CDC* /*pDC*/,
                             int /*nItem*/,
                             UINT /*nState*/,
                             LPARAM /*lParam*/)
 { return false; }

 //
 // Callbacks for each sub item
 //

 // return a pointer to the font to use for this sub item.
 // return NULL to use default
 virtual CFont* FontForSubItem(int /*nItem*/,
                               int /*nSubItem*/,
                               UINT /*nState*/,
                               LPARAM /*lParam*/)
 { return NULL; }

 // return the text color to use for this sub item
 // return CLR_DEFAULT to use default
 virtual COLORREF TextColorForSubItem(int /*nItem*/,
                                      int /*nSubItem*/,
                                      UINT /*nState*/,
                                      LPARAM /*lParam*/)
 { return CLR_DEFAULT; }

 // return the background color to use for this sub item
 // return CLR_DEFAULT to use default
 virtual COLORREF BkColorForSubItem(int /*nItem*/,
                                    int /*nSubItem*/,
                                    UINT /*nState*/,
                                    LPARAM /*lParam*/)
 { return CLR_DEFAULT; }

 // do we want to do the drawing for this sub item ourselves?
 virtual bool IsSubItemDraw(int /*nItem*/,
                            int /*nSubItem*/,
                            UINT /*nState*/,
                            LPARAM /*lParam*/)
 { return false; }

 // if we are doing the drawing ourselves
 // override and put the code in here
 // and return TRUE if we did indeed do
 // all the drawing ourselves
 virtual bool OnSubItemDraw(CDC* /*pDC*/,
                            int /*nItem*/,
                            int /*nSubItem*/,
                            UINT /*nState*/,
                            LPARAM /*lParam*/)
 { return false; }

 // do we want to be notified when the
 // painting has finished
 virtual bool IsNotifySubItemPostPaint(int /*nItem*/,
                                       int /*nSubItem*/,
                                       UINT /*nState*/,
                                       LPARAM /*lParam*/)
 { return false; }

 // do we want to do any drawing after
 // the list control is finished
 virtual bool IsSubItemPostDraw() { return false; }

 // if we are doing the drawing afterwards ourselves
 // override and put the code in here
 // the return value is not used here
 virtual bool OnSubItemPostDraw(CDC* /*pDC*/,
                                int /*nItem*/,
                                int /*nSubItem*/,
                                UINT /*nState*/,
                                LPARAM /*lParam*/)
 { return false; }

There is quite a bit of code here as well. Again, there is good new. Each of the virtual functions performs a simple and well-defined task. The OnCustomdraw function does all the housework and ties everything together.

Well, that about wraps it up for our CListCtrlWithCustomDraw class. The last step is to derive a class from CListCtrlWithCustomDraw.

Using the CListCtrlWithCustomDraw Class

Again, we can use the WizardBar to create a new class. And as before it can be done with a little hacking. Start off by defining a class call CMyListCtrl, and say that it is an MFC class derived from CListCtrl. Then edit the .h and .cpp files that are generated, change all occurrences of CListCtrl to CListCtrlWithCustomDraw, and finally add a #include "ListCtrlWithCustomDraw.h" line to the .h file, just before the class declaration. Once you have the skeleton CMyListCtrl, we can override some of the virtual functions to change the appearance of the control.

For this example, we will paint the entire control in cyan, and then make the individual cells in the list control alternate in colors to give a checkerboard appearance. OK, it is not very pretty, but it illustrates some of the possibilities.

Here are the virtual functions we will override in CMyListCtrl.

virtual bool IsDraw();

virtual bool OnDraw(CDC* pDC, const CRect& r);

virtual bool IsNotifyItemDraw();

virtual bool IsNotifySubItemDraw(int nItem,
                                 UINT nState,
                                 LPARAM lParam);

virtual COLORREF TextColorForSubItem(int nItem,
                                     int nSubItem,
                                     UINT nState,
                                     LPARAM lParam);

virtual COLORREF BkColorForSubItem(int nItem,
                                   int nSubItem,
                                   UINT nState,
                                   LPARAM lParam);

And here are their implementations:

bool CMyListCtrl::IsDraw() {
 return true;
}
bool CMyListCtrl::OnDraw(CDC* pDC, const CRect& r) {
 CBrush brush(RGB(0,255,255));	// cyan
 pDC->FillRect(r,&brush);
 return false; // do default drawing as well
}

bool CMyListCtrl::IsNotifyItemDraw() {
 return true;
}
bool CMyListCtrl::IsNotifySubItemDraw(int /*nItem*/,
                                      UINT /*nState*/,
                                      LPARAM /*lParam*/) {
 return true;
}
COLORREF CMyListCtrl::TextColorForSubItem(int nItem,
                                          int nSubItem,
                                          UINT /*nState*/,
                                          LPARAM /*lParam*/) {
 if (0 == (nItem+nSubItem)%2) {
  return RGB(255,255,0);	// yellow
 } else {
  return CLR_DEFAULT;
 }
}
COLORREF CMyListCtrl::BkColorForSubItem(int nItem,
                                        int nSubItem,
                                        UINT /*nState*/,
                                        LPARAM /*lParam*/) {
 if (0 == (nItem+nSubItem)%2) {
  return RGB(255,0,255); // magenta
 } else {
  return CLR_DEFAULT;
 }
}

And remember that overriding IsDraw and OnDraw lets you either draw the entire control yourself or just to do some extra work before the default drawing process. In this case, the OnDraw function fills the control with cyan and then returns false to indicate that we still want the default drawing process to continue

If you want to individually change the colors of each cell, override IsNotifyItemDraw and IsNotifySubItemDraw. If you want the sub-items to be custom drawn by returning true from IsNotifySubItemDraw, then you also need to return true from IsNotifyItemDraw. If IsNotifyItemDraw returns false, then there will be no sub-item custom drawing either.

In the TextColorForSubItem, simply do some arithmetic with the item and sub-item number to select either a different color or the default color for the control.

To test this out, add a list control to the main dialog for this application. In the dialog editor, ctrl+double-click on it to associate it with a CMyListCtrl member called m_listctrl., then edit the list control properties in the dialog and set the style to use report view. Then, add some code to the OnInitDialog to define the columns and fill in some data.

m_listctrl.InsertColumn(0,"label",LVCFMT_LEFT,60,0);
m_listctrl.InsertColumn(1,"first",LVCFMT_LEFT,40,1);
m_listctrl.InsertColumn(2,"second",LVCFMT_LEFT,30,2);
m_listctrl.InsertColumn(3,"third",LVCFMT_LEFT,20,3);

int row;
row = m_listctrl.InsertItem(0,"row1");
m_listctrl.SetItem(row,1,LVIF_TEXT,"aaa",0,0,0,0);
m_listctrl.SetItem(row,2,LVIF_TEXT,"bbb",0,0,0,0);
m_listctrl.SetItem(row,3,LVIF_TEXT,"ccc",0,0,0,0);
row = m_listctrl.InsertItem(1,"row2");
m_listctrl.SetItem(row,1,LVIF_TEXT,"AAA",0,0,0,0);
m_listctrl.SetItem(row,2,LVIF_TEXT,"BBB",0,0,0,0);
m_listctrl.SetItem(row,3,LVIF_TEXT,"CCC",0,0,0,0);
row = m_listctrl.InsertItem(2,"row3");
m_listctrl.SetItem(row,1,LVIF_TEXT,"X",0,0,0,0);
m_listctrl.SetItem(row,2,LVIF_TEXT,"YY",0,0,0,0);
m_listctrl.SetItem(row,3,LVIF_TEXT,"ZZZ",0,0,0,0);

The result is a dialog with the cyan background and checkerboard.

Summary

By deriving from ClistCtrlWithCustomDraw and overriding appropriate virtual functions, you can achieve all sorts of result–everything from simple color or font changes to completely drawing all or part of the list control yourself.

The next step is to add a handler for the NM_CUSTOMDRAW notification message. Usually, to add a handler, one can simply right-click on CListCtrlWithCustomDraw in the class view, or use the WizardBar, and "Add windows message handler." However, this time there is a catch. NM_CUSTOMDRAW is nowhere to be seen in the list of available messages.

Well, it looks like the wizard is not going to do the job this time. However, we can get it to help. I find the easiest way to add a handler is to pick a similar message and then edit the resultant code. Even though the Wizard did not know about the message in the first place, it will quite happily work with the modified handler that results. In this case, I used the NM_OUTOFMEMORY. However, in order to reduce the amount of editing needed, I changed the name of the handler function to "OnCustomdraw". 

Now you can dive into the generated source code and manually edit the message map in the source file and change:

ON_NOTIFY_REFLECT(NM_OUTOFMEMORY, OnCustomdraw)

to read:

ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnCustomdraw)

When you look at the resulting OnCustomdraw function there are two arguments: pNMHDR and pResult. pResult is where we will set the flags that indicate when we want further custom draw messages and whether or not to use the default painting. For a List View control, the pNMHDR is actually a pointer to a NMLVCUSTOMDRAW structure (notification message for ListView custom draw) that tells us about the current drawing stage, what item or subitems we are looking at, and so forth.

We could edit the function declaration so it passes a NMLVCUSTOMDRAW*, but instead we can safely cast the pNMHDR from NMHDR* to NMLVCUSTOMDRAW* to give access to the data, even though these are distinct and separate structures.

As an aside, those from a C++ background may ask why NMLVCUSTOMDRAW does not simply derive from NMHDR in the first place. Well, that sort of thing just does not happen in the Windows SDK API. Because Microsoft designed the SDK API to work with both C and C++, it cannot use any C++ specific language features. That means all the structures in the SDK are PODs (plain old data structures) with no member functions, inheritance, and so on.

But all is not lost.

To get a similar effect to inheritance the SDK uses a C technique; the first member of a ‘derived’ struct is an instance of the ‘base’ struct. In this particular case, we have (with simplified declarations):

typedef struct {
 HWND hwndFrom;
 UINT idFrom;
 UINT code;
} NMHDR;

typedef struct {
 NMHDR  hdr;
 DWORD  dwDrawStage;
} NMCUSTOMDRAW;

typedef struct {
 NMCUSTOMDRAW nmcd;
 COLORREF clrText;
} NMLVCUSTOMDRAW;

Because NMLVCUSTOMDRAW is a POD, a pointer to a NMLVCUSTOMDRAW is also a pointer to its first member (nmcd), in other words a pointer to a NMCUSTOMDRAW. This in turn is a pointer to the first member of NMCUSTOMDRAW (hdr), which is a NMHDR. Therefore, it is quite legitimate to pass a pointer to a NMCUSTOMDRAW structure using a pointer to a NMHDR and cast it as required.

Now, on with the show.

The CListCtrlWithCustomDraw Class

In the OnCustomdraw for CListCtrlWithCustomDraw, I provide a generic handler for custom draw notifications. The bulk of the code is a switch statement on the draw stage we are up to (refer to my previous article). At each of the pre-paint stages, I call virtual functions to get color information, fonts required, and to take over (or augment) the drawing process. I also allow for extra drawing during the post-paint stages.

Let’s look at the fleshed out OnCustomdraw function for CListCtrlWithCustomDraw:

void CListCtrlWithCustomDraw::OnCustomdraw(NMHDR* pNMHDR,
                                           LRESULT* pResult)
{
 // first, lets extract data from
 // the message for ease of use later
 NMLVCUSTOMDRAW* pNMLVCUSTOMDRAW = (NMLVCUSTOMDRAW*)pNMHDR;

 // we'll copy the device context into hdc
 // but won't convert it to a pDC* until (and if)
 // we need it as this requires a bit of work
 // internally for MFC to create temporary CDC
 // objects
 HDC hdc = pNMLVCUSTOMDRAW->nmcd.hdc;
 CDC* pDC = NULL;

 // here is the item info
 // note that we don't get the subitem
 // number here, as this may not be
 // valid data except when we are
 // handling a sub item notification
 // so we'll do that separately in
 // the appropriate case statements
 // below.
 int nItem = pNMLVCUSTOMDRAW->nmcd.dwItemSpec;
 UINT nState = pNMLVCUSTOMDRAW->nmcd.uItemState;
 LPARAM lParam = pNMLVCUSTOMDRAW->nmcd.lItemlParam;

 // next we set up flags that will control
 // the return value for *pResult
 bool bNotifyPostPaint = false;
 bool bNotifyItemDraw = false;
 bool bNotifySubItemDraw = false;
 bool bSkipDefault = false;
 bool bNewFont = false;

 // what we do next depends on the
 // drawing stage we are processing
 switch (pNMLVCUSTOMDRAW->nmcd.dwDrawStage) {
  case CDDS_PREPAINT:
  {
   // PrePaint
   m_pOldItemFont = NULL;
   m_pOldSubItemFont = NULL;

   bNotifyPostPaint = IsNotifyPostPaint();
   bNotifyItemDraw = IsNotifyItemDraw();

   // do we want to draw the control ourselves?
   if (IsDraw()) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     CRect r(pNMLVCUSTOMDRAW->nmcd.rc);

    // do the drawing
    if (OnDraw(pDC,r)) {
     // we drew it all ourselves
     // so don't do default
     bSkipDefault = true;
    }
   }
  }
  break;

  case CDDS_ITEMPREPAINT:
  {
   // Item PrePaint
   m_pOldItemFont = NULL;

   bNotifyPostPaint = IsNotifyItemPostPaint(nItem,nState,lParam);
   bNotifySubItemDraw = IsNotifySubItemDraw(nItem,nState,lParam);

   // set up the colors to use
   pNMLVCUSTOMDRAW->clrText =
    TextColorForItem(nItem,nState,lParam);

   pNMLVCUSTOMDRAW->clrTextBk =
    BkColorForItem(nItem,nState,lParam);

   // set up a different font to use, if any
   CFont* pNewFont = FontForItem(nItem,nState,lParam);
   if (pNewFont) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     m_pOldItemFont = pDC->SelectObject(pNewFont);

    bNotifyPostPaint = true; // need to restore font
   }

   // do we want to draw the item ourselves?
   if (IsItemDraw(nItem,nState,lParam)) {
    if (! pDC) pDC = CDC::FromHandle(hdc);

    if (OnItemDraw(pDC,nItem,nState,lParam)) {
     // we drew it all ourselves
     // so don't do default
     bSkipDefault = true;
    }
   }
  }
  break;

  case CDDS_ITEMPREPAINT|CDDS_SUBITEM:
  {
   // Sub Item PrePaint
   // set sub item number (data will be valid now)
   int nSubItem = pNMLVCUSTOMDRAW->iSubItem;

   m_pOldSubItemFont = NULL;

   bNotifyPostPaint =
    IsNotifySubItemPostPaint(nItem, nSubItem, nState, lParam);

   // set up the colors to use
   pNMLVCUSTOMDRAW->clrText =
    TextColorForSubItem(nItem,nSubItem,nState,lParam);

   pNMLVCUSTOMDRAW->clrTextBk =
    BkColorForSubItem(nItem,nSubItem,nState,lParam);

   // set up a different font to use, if any
   CFont* pNewFont =
    FontForSubItem(nItem, nSubItem, nState, lParam);

   if (pNewFont) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     m_pOldSubItemFont = pDC->SelectObject(pNewFont);

     bNotifyPostPaint = true;    // need to restore font
    }

    // do we want to draw the item ourselves?
    if (IsSubItemDraw(nItem,nSubItem,nState,lParam)) {
     if (! pDC) pDC = CDC::FromHandle(hdc);
      if (OnSubItemDraw(pDC,nItem,nSubItem,nState,lParam)) {

     // we drew it all ourselves
     // so don't do default
     bSkipDefault = true;
    }
   }
  }
  break;

  case CDDS_ITEMPOSTPAINT|CDDS_SUBITEM:
  {
   // Sub Item PostPaint
   // set sub item number (data will be valid now)
   int nSubItem = pNMLVCUSTOMDRAW->iSubItem;

   // restore old font if any
   if (m_pOldSubItemFont) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     pDC->SelectObject(m_pOldSubItemFont);

    m_pOldSubItemFont = NULL;
   }

   // do we want to do any extra drawing?
   if (IsSubItemPostDraw()) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
    OnSubItemPostDraw(pDC,nItem,nSubItem,nState,lParam);
   }
  }
  break;

  case CDDS_ITEMPOSTPAINT:
  {
   // Item PostPaint
   // restore old font if any
   if (m_pOldItemFont) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
    pDC->SelectObject(m_pOldItemFont);
    m_pOldItemFont = NULL;
   }

   // do we want to do any extra drawing?
   if (IsItemPostDraw()) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     OnItemPostDraw(pDC,nItem,nState,lParam);
   }
  }
  break;

  case CDDS_POSTPAINT:
  {
   // Item PostPaint
   // do we want to do any extra drawing?
   if (IsPostDraw()) {
    if (! pDC) pDC = CDC::FromHandle(hdc);
     CRect r(pNMLVCUSTOMDRAW->nmcd.rc);

    OnPostDraw(pDC,r);
   }
  }
  break;
 }

 ASSERT(CDRF_DODEFAULT==0);
 *pResult = 0;
 if (bNotifyPostPaint) {
  *pResult |= CDRF_NOTIFYPOSTPAINT;
 }

 if (bNotifyItemDraw) {
  *pResult |= CDRF_NOTIFYITEMDRAW;
 }

 if (bNotifySubItemDraw) {
  *pResult |= CDRF_NOTIFYSUBITEMDRAW;
 }

 if (bNewFont) {
  *pResult |= CDRF_NEWFONT;
 }

 if (bSkipDefault) {
  *pResult |= CDRF_SKIPDEFAULT;
 }

 if (*pResult == 0) {
  // redundant as CDRF_DODEFAULT==0 anyway
  // but shouldn't depend on this in our code
  *pResult = CDRF_DODEFAULT;
 }
}

Phew! That’s a fair bit of code. The good news? There’s less code left for us to write when we derive from CListCtrlWithCustomDraw.

To make this work, those virtual functions need to be defined. The defaults for these functions are to do nothing; so using the CListCtrlWithCustomDraw on its own will work the same as a standard list control. It is only when you derive from this class and override some of the virtual functions that the ListView control changes appearance. If you do not override them, then you get the standard behaviour.

To start with, here are the additions to the class declaration for CListCtrlWithCustomDraw:

protected:
 CFont* m_pOldItemFont;
 CFont* m_pOldSubItemFont;

 //
 // Callbacks for whole control
 //

 // do we want to do the drawing ourselves?
 virtual bool IsDraw() { return false; }

 // if we are doing the drawing ourselves
 // override and put the code in here
 // and return TRUE if we did indeed do
 // all the drawing ourselves
 virtual bool OnDraw(CDC* /*pDC*/, const CRect& /*r*/)
 { return false; }

 // do we want to handle custom draw for
 // individual items
 virtual bool IsNotifyItemDraw() { return false; }

 // do we want to be notified when the
 // painting has finished
 virtual bool IsNotifyPostPaint() { return false; }

 // do we want to do any drawing after
 // the list control is finished
 virtual bool IsPostDraw() { return false; }

 // if we are doing the drawing afterwards ourselves
 // override and put the code in here
 // the return value is not used here
 virtual bool OnPostDraw(CDC* /*pDC*/, const CRect& /*r*/)
 { return false; }

 //
 // Callbacks for each item
 //

 // return a pointer to the font to use for this item.
 // return NULL to use default
 virtual CFont* FontForItem(int /*nItem*/,
                            UINT /*nState*/,
                            LPARAM /*lParam*/)
 { return NULL; }

 // return the text color to use for this item
 // return CLR_DEFAULT to use default
 virtual COLORREF TextColorForItem(int /*nItem*/,
                                   UINT /*nState*/,
                                   LPARAM /*lParam*/)
 { return CLR_DEFAULT; }

 // return the background color to use for this item
 // return CLR_DEFAULT to use default
 virtual COLORREF BkColorForItem(int /*nItem*/,
                                 UINT /*nState*/,
                                 LPARAM /*lParam*/)
 { return CLR_DEFAULT; }

 // do we want to do the drawing for this item ourselves?
 virtual bool IsItemDraw(int /*nItem*/,
                         UINT /*nState*/,
                         LPARAM /*lParam*/)
 { return false; }

 // if we are doing the drawing ourselves
 // override and put the code in here
 // and return TRUE if we did indeed do
 // all the drawing ourselves
 virtual bool OnItemDraw(CDC* /*pDC*/,
                         int /*nItem*/,
                         UINT /*nState*/,
                         LPARAM /*lParam*/)
 { return false; }

 // do we want to handle custom draw for
 // individual sub items
 virtual bool IsNotifySubItemDraw(int /*nItem*/,
                                  UINT /*nState*/,
                                  LPARAM /*lParam*/)
 { return false; }

 // do we want to be notified when the
 // painting has finished
 virtual bool IsNotifyItemPostPaint(int /*nItem*/,
                                    UINT /*nState*/,
                                    LPARAM /*lParam*/)
 { return false; }

 // do we want to do any drawing after
 // the list control is finished
 virtual bool IsItemPostDraw() { return false; }

 // if we are doing the drawing afterwards ourselves
 // override and put the code in here
 // the return value is not used here
 virtual bool OnItemPostDraw(CDC* /*pDC*/,
                             int /*nItem*/,
                             UINT /*nState*/,
                             LPARAM /*lParam*/)
 { return false; }

 //
 // Callbacks for each sub item
 //

 // return a pointer to the font to use for this sub item.
 // return NULL to use default
 virtual CFont* FontForSubItem(int /*nItem*/,
                               int /*nSubItem*/,
                               UINT /*nState*/,
                               LPARAM /*lParam*/)
 { return NULL; }

 // return the text color to use for this sub item
 // return CLR_DEFAULT to use default
 virtual COLORREF TextColorForSubItem(int /*nItem*/,
                                      int /*nSubItem*/,
                                      UINT /*nState*/,
                                      LPARAM /*lParam*/)
 { return CLR_DEFAULT; }

 // return the background color to use for this sub item
 // return CLR_DEFAULT to use default
 virtual COLORREF BkColorForSubItem(int /*nItem*/,
                                    int /*nSubItem*/,
                                    UINT /*nState*/,
                                    LPARAM /*lParam*/)
 { return CLR_DEFAULT; }

 // do we want to do the drawing for this sub item ourselves?
 virtual bool IsSubItemDraw(int /*nItem*/,
                            int /*nSubItem*/,
                            UINT /*nState*/,
                            LPARAM /*lParam*/)
 { return false; }

 // if we are doing the drawing ourselves
 // override and put the code in here
 // and return TRUE if we did indeed do
 // all the drawing ourselves
 virtual bool OnSubItemDraw(CDC* /*pDC*/,
                            int /*nItem*/,
                            int /*nSubItem*/,
                            UINT /*nState*/,
                            LPARAM /*lParam*/)
 { return false; }

 // do we want to be notified when the
 // painting has finished
 virtual bool IsNotifySubItemPostPaint(int /*nItem*/,
                                       int /*nSubItem*/,
                                       UINT /*nState*/,
                                       LPARAM /*lParam*/)
 { return false; }

 // do we want to do any drawing after
 // the list control is finished
 virtual bool IsSubItemPostDraw() { return false; }

 // if we are doing the drawing afterwards ourselves
 // override and put the code in here
 // the return value is not used here
 virtual bool OnSubItemPostDraw(CDC* /*pDC*/,
                                int /*nItem*/,
                                int /*nSubItem*/,
                                UINT /*nState*/,
                                LPARAM /*lParam*/)
 { return false; }

There is quite a bit of code here as well. Again, there is good new. Each of the virtual functions performs a simple and well-defined task. The OnCustomdraw function does all the housework and ties everything together.

Well, that about wraps it up for our CListCtrlWithCustomDraw class. The last step is to derive a class from CListCtrlWithCustomDraw.

Using the CListCtrlWithCustomDraw Class

Again, we can use the WizardBar to create a new class. And as before it can be done with a little hacking. Start off by defining a class call CMyListCtrl, and say that it is an MFC class derived from CListCtrl. Then edit the .h and .cpp files that are generated, change all occurrences of CListCtrl to CListCtrlWithCustomDraw, and finally add a #include "ListCtrlWithCustomDraw.h" line to the .h file, just before the class declaration. Once you have the skeleton CMyListCtrl, we can override some of the virtual functions to change the appearance of the control.

For this example, we will paint the entire control in cyan, and then make the individual cells in the list control alternate in colors to give a checkerboard appearance. OK, it is not very pretty, but it illustrates some of the possibilities.

Here are the virtual functions we will override in CMyListCtrl.

virtual bool IsDraw();

virtual bool OnDraw(CDC* pDC, const CRect& r);

virtual bool IsNotifyItemDraw();

virtual bool IsNotifySubItemDraw(int nItem,
                                 UINT nState,
                                 LPARAM lParam);

virtual COLORREF TextColorForSubItem(int nItem,
                                     int nSubItem,
                                     UINT nState,
                                     LPARAM lParam);

virtual COLORREF BkColorForSubItem(int nItem,
                                   int nSubItem,
                                   UINT nState,
                                   LPARAM lParam);

And here are their implementations:

bool CMyListCtrl::IsDraw() {
 return true;
}
bool CMyListCtrl::OnDraw(CDC* pDC, const CRect& r) {
 CBrush brush(RGB(0,255,255));	// cyan
 pDC->FillRect(r,&brush);
 return false; // do default drawing as well
}

bool CMyListCtrl::IsNotifyItemDraw() {
 return true;
}
bool CMyListCtrl::IsNotifySubItemDraw(int /*nItem*/,
                                      UINT /*nState*/,
                                      LPARAM /*lParam*/) {
 return true;
}
COLORREF CMyListCtrl::TextColorForSubItem(int nItem,
                                          int nSubItem,
                                          UINT /*nState*/,
                                          LPARAM /*lParam*/) {
 if (0 == (nItem+nSubItem)%2) {
  return RGB(255,255,0);	// yellow
 } else {
  return CLR_DEFAULT;
 }
}
COLORREF CMyListCtrl::BkColorForSubItem(int nItem,
                                        int nSubItem,
                                        UINT /*nState*/,
                                        LPARAM /*lParam*/) {
 if (0 == (nItem+nSubItem)%2) {
  return RGB(255,0,255); // magenta
 } else {
  return CLR_DEFAULT;
 }
}

And remember that overriding IsDraw and OnDraw lets you either draw the entire control yourself or just to do some extra work before the default drawing process. In this case, the OnDraw function fills the control with cyan and then returns false to indicate that we still want the default drawing process to continue

If you want to individually change the colors of each cell, override IsNotifyItemDraw and IsNotifySubItemDraw. If you want the sub-items to be custom drawn by returning true from IsNotifySubItemDraw, then you also need to return true from IsNotifyItemDraw. If IsNotifyItemDraw returns false, then there will be no sub-item custom drawing either.

In the TextColorForSubItem, simply do some arithmetic with the item and sub-item number to select either a different color or the default color for the control.

To test this out, add a list control to the main dialog for this application. In the dialog editor, ctrl+double-click on it to associate it with a CMyListCtrl member called m_listctrl., then edit the list control properties in the dialog and set the style to use report view. Then, add some code to the OnInitDialog to define the columns and fill in some data.

m_listctrl.InsertColumn(0,"label",LVCFMT_LEFT,60,0);
m_listctrl.InsertColumn(1,"first",LVCFMT_LEFT,40,1);
m_listctrl.InsertColumn(2,"second",LVCFMT_LEFT,30,2);
m_listctrl.InsertColumn(3,"third",LVCFMT_LEFT,20,3);

int row;
row = m_listctrl.InsertItem(0,"row1");
m_listctrl.SetItem(row,1,LVIF_TEXT,"aaa",0,0,0,0);
m_listctrl.SetItem(row,2,LVIF_TEXT,"bbb",0,0,0,0);
m_listctrl.SetItem(row,3,LVIF_TEXT,"ccc",0,0,0,0);
row = m_listctrl.InsertItem(1,"row2");
m_listctrl.SetItem(row,1,LVIF_TEXT,"AAA",0,0,0,0);
m_listctrl.SetItem(row,2,LVIF_TEXT,"BBB",0,0,0,0);
m_listctrl.SetItem(row,3,LVIF_TEXT,"CCC",0,0,0,0);
row = m_listctrl.InsertItem(2,"row3");
m_listctrl.SetItem(row,1,LVIF_TEXT,"X",0,0,0,0);
m_listctrl.SetItem(row,2,LVIF_TEXT,"YY",0,0,0,0);
m_listctrl.SetItem(row,3,LVIF_TEXT,"ZZZ",0,0,0,0);

The result is a dialog with the cyan background and checkerboard.

Summary

By deriving from ClistCtrlWithCustomDraw and overriding appropriate virtual functions, you can achieve all sorts of result–everything from simple color or font changes to completely drawing all or part of the list control yourself.

2005年07月21日

从今天开始将陆续翻译Peer-to-Peer (P2P) communication across middleboxes这篇文章,并没有按照章节次序来,请读者见谅。

原文版权:Copyright (C) The Internet Society (2003).  All Rights Reserved.

原文地址:http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt

 

3.4. UDP port number prediction UPD端口号预言

A variant of the UDP hole punching technique discussed above exists that allows P2P UDP sessions to be created in the presence of some symmetric NATs.  This method is sometimes called the "N+1" technique [BIDIR] and is explored in detail by Takeda [SYM-STUN]. The method works by analyzing the behavior of the NAT and attempting to predict the public port numbers it will assign to future sessions.  

Consider again the situation in which two clients, A and B, each behind a separate NAT, have each established UDP connections with a permanently addressable server S:

   让我们来考虑这样一种情况,有两个客户端 A B,他们都藏在不同的NAT后面,他们都开放了一个UDP连接给具有固定IPServer S:如下图

 

   NAT A has assigned its own UDP port 62000 to the communication session between A and S, and NAT B has assigned its port 31000 to the session between B and S.  By communicating through server S, A and B learn each other’s public IP addresses and port numbers as observed   by S.  Client A now starts sending UDP messages to port 31001 at address 138.76.29.7 (note the port number increment), and client B simultaneously starts sending messages to port 62001 at address 155.99.25.11.  If NATs A and B assign port numbers to new sessions  sequentially, and if not much time has passed since the A-S and B-S sessions were initiated, then a working bi-directional communication channel between A and B should result.

 

   A’s messages to B cause NAT A  to open up a new session, to which NAT A will (hopefully) assign public port number 62001, because 62001 is next in sequence after the  port number 62000 it previously assigned to the session between A and S.  Similarly, B’s messages to A will cause NAT B to open a new   session, to which it will (hopefully) assign port number 31001.  If both clients have correctly guessed the port numbers each NAT assigns to the new sessions, then a bi-directional UDP communication channel will have been established as shown below.

 

   NAT A 分配了它自己的UDP端口62000,用来保持 客户端A 服务器S 的通信会话, NAT B 也分配了31000端口,用来保持 客户端B 服务器S 的通信会话。通过与 服务器S的对话,客户端A 客户端B 都相互知道了对方所映射的真实IP和端口。

   客户端A发送一条UDP消息到 138.76.29.7:31001(请注意到端口号的增加)同时 客户端B发送一条UDP消息到 155.99.25.11:62001。如果NAT A NAT B继续分配端口给新的会话,并且从A-SB-S的会话时间消耗得并不多的话,那么一条处于客户端A客户端B之间的双向会话通道就建立了。

   客户端A发出的消息送达B导致了NAT A打开了一个新的会话,并且我们希望 NAT A将会指派62001端口给这个新的会话,因为62001是继62000后,NAT会自动指派给 从服务器S客户端A之间的新会话的端口号;类似的,客户端B发出的消息送达A导致了 NAT B打开了一个新的会话,并且我们希望 NAT B 将会指派31001这个端口给新的会话;如果两个客户端都正确的猜测到了对方新会话被指派的端口号,那么这个 客户端A-客户端B的双向连接就被打通了。其结果如下图所示:

  

   Obviously there are many things that can cause this trick to fail. If the predicted port number at either NAT already happens to be in use by an unrelated session, then the NAT will skip over that port number and the connection attempt will fail.  If either NAT sometimes or always chooses port numbers non-sequentially, then the trick will fail. 
  
   If a different client behind NAT A (or B respectively) opens up a new outgoing UDP connection to any external destination after A (B) establishes its connection with S but before sending its first message to B (A), then the unrelated client will inadvertently "steal" the desired port number.  This trick is therefore much less likely to work when either NAT involved is under load.

  

明显的,有许多因素会导致这个方法失败:如果这个预言的新端口(6200131001) 恰好已经被一个不相关的会话所使用,那么NAT就会跳过这个端口号,这个连接就会宣告失败;如果两个NAT有时或者总是不按照顺序来生成新的端口号,那么这个方法也是行不通的。

  

如果隐藏在NAT A后的一个不同的客户端X(或者在NAT B后)打开了一个新的外出”UDP 连接,并且无论这个连接的目的如何;只要这个动作发生在 客户端A 建立了与服务器S 的连接之后,客户端A 客户端B 建立连接之前;那么这个无关的客户端X 就会趁人不备地” 到这个我们渴望分配的端口。所以,这个方法变得如此脆弱而且不堪一击,只要任何一个NAT方包含以上碰到的问题,这个方法都不会奏效。

      
 
  Since in practice a P2P application implementing this trick would still need to work if the NATs are cone NATs, or if one is a cone NAT and the other is a symmetric NAT, the application would need to detect beforehand what kind of NAT is involved on either end [STUN] and modify its behavior accordingly, increasing the complexity of the algorithm and the general brittleness of the network.  

 

   Finally, port number prediction has no chance of working if either client is behind two or more levels of NAT and the NAT(s) closest to the client are symmetric.  For all of these reasons, it is NOT recommended that new applications implement this trick; it is mentioned here for historical and informational purposes.

 

   自从使用这种方法来实践P2P的应用程序以来,在处于 cone NAT 系列的网络环境中这个方法还是实用的;如果有一方为 cone NAT 而另外一方为 symmetric NAT,那么应用程序就应该预先发现另外一方的 NAT 是什么类型,再做出正确的行为来处理通信,这样就增大了算法的复杂度,并且降低了在真实网络环境中的普适性。

    最后,如果P2P的一方处在两级或者两级以上的NAT下面,并且这些NATs 接近这个客户端是 symmetric的话,端口号预言 是无效的!

    因此,并不推荐使用这个方法来写新的P2P应用程序,这也是历史的经验和教训!

3.3. UDP hole punching  UDP打洞技术

    The third technique, and the one of primary interest in this document, is widely known as "UDP Hole Punching." UDP hole punching relies on the properties of common firewalls and cone NATs to allow appropriately designed peer-to-peer applications to "punch holes" through the middlebox and establish direct connectivity with each other, even when both communicating hosts may lie behind middleboxes. This technique was mentioned briefly in section 5.1 of RFC 3027 [NAT-PROT], and has been informally described elsewhere on the Internet [KEGEL] and used in some recent protocols [TEREDO, ICE]. As the name implies, unfortunately, this technique works reliably only with UDP.

 

    第三种技术,也是这篇文章主要要研究的,就是非常有名的“UDP打洞技术UDP打洞技术依赖于由公共防火墙和cone NAT,允许适当的有计划的端对端应用程序通过NAT打洞,即使当双方的主机都处于NAT之后。这种技术在 RFC30275.1[NAT PROT] 中进行了重点介绍,并且在Internet[KEGEL]中进行了非正式的描叙,还应用到了最新的一些协议,例如[TEREDO,ICE]协议中。不过,我们要注意的是,如其名,UDP打洞技术的可靠性全都要依赖于UDP

 

     We will consider two specific scenarios, and how applications can be designed to handle both of them gracefully. In the first situation, representing the common case, two clients desiring direct peer-to- peer communication reside behind two different NATs. In the second, the two clients actually reside behind the same NAT, but do not necessarily know that they do.

 

     这里将考虑两种典型场景,来介绍连接的双方应用程序如何按照计划的进行通信的,第一种场景,我们假设两个客户端都处于不同的NAT之后;第二种场景,我们假设两个客户端都处于同一个NAT之后,但是它们彼此都不知道(他们在同一个NAT)

 

3.3.1. Peers behind different NATs  处于不同NAT之后的客户端通信

 

     Suppose clients A and B both have private IP addresses and lie behind different network address translators. The peer-to-peer application running on clients A and B and on server S each use UDP port 1234.? A and B have each initiated UDP communication sessions with server S, causing NAT A to assign its own public UDP port 62000 for A’s session with S, and causing NAT B to assign its port 31000 to B’s session with S, respectively.

 

    我们假设 Client A 和 Client B 都拥有自己的私有IP地址,并且都处在不同的NAT之后,端对端的程序运行于 CLIENT A,CLIENT B,S之间,并且它们都开放了UDP端口1234 CLIENT ACLIENT B首先分别与S建立通信会话,这时NAT A把它自己的UDP端口62000分配给CLIENT AS的会话,NAT B也把自己的UDP端口31000分配给CLIENT BS的会话。如下图所示:

     Now suppose that client A wants to establish a UDP communication session directly with client B.? If A simply starts sending UDP messages to B’s public address, 138.76.29.7:31000, then NAT B will typically discard these incoming messages (unless it is a full cone NAT), because the source address and port number does not match those of S, with which the original outgoing session was established. Similarly, if B simply starts sending UDP messages to A’s public address, then NAT A will typically discard these messages.

 

     假如这个时候 CLIENT A 想与 CLIENT B建立一条UDP通信直连,如果 CLIENT A只是简单的发送一个UDP信息到CLIENT B的公网地址138.76.29.7:31000的话,NAT B会不加考虑的将这个信息丢弃(除非NAT B是一个 full cone NAT),因为 这个UDP信息中所包含的地址信息,与CLIENT B和服务器S建立连接时存储在NAT B中的服务器S的地址信息不符。同样的,CLIENT B如果做同样的事情,发送的UDP信息也会被 NAT A 丢弃。

 

     Suppose A starts sending UDP messages to B’s public address, however, and simultaneously relays a request through server S to B, asking B to start sending UDP messages to A’s public address.? A’s outgoing messages directed to B’s public address (138.76.29.7:31000) cause NAT A to open up a new communication session between A’s private address and B’s public address. At the same time, B’s messages to A’s public address (155.99.25.11:62000) cause NAT B to open up a new communication session between B’s private address and A’s public address. Once the new UDP sessions have been opened up in each direction, client A and B can communicate with each other directly without further burden on the "introduction" server S.

 

    假如 CLIENT A 开始发送一个 UDP 信息到 CLIENT B 的公网地址上,与此同时,他又通过S中转发送了一个邀请信息给CLIENT B,请求CLIENT B也给CLIENT A发送一个UDP信息到 CLIENT A的公网地址上。这时CLIENT ACLIENT B的公网IP(138.76.29.7:31000)发送的信息导致 NAT A 打开一个处于 CLIENT A的私有地址和CLIENT B的公网地址之间的新的通信会话,与此同时,NAT B 也打开了一个处于CLIENT B的私有地址和CLIENT A的公网地址(155.99.25.11:62000)之间的新的通信会话。一旦这个新的UDP会话各自向对方打开了,CLIENT ACLIENT B之间就可以直接通信,而无需S来牵线搭桥了。(这就是所谓的打洞技术)!

 

     The UDP hole punching technique has several useful properties. Once a direct peer-to-peer UDP connection has been established between two clients behind middleboxes, either party on that connection can in turn take over the role of "introducer" and help the other party establish peer-to-peer connections with additional peers, minimizing the load on the initial introduction server S. The application does not need to attempt to detect explicitly what kind of middlebox it is behind, if any [STUN], since the procedure above will establish peer- to-peer communication channels equally well if either or both clients do not happen to be behind a middlebox.? The hole punching technique even works automatically with multiple NATs, where one or both clients are removed from the public Internet via two or more levels of address translation.

 

     UDP打洞技术有很多实用的地方:第一,一旦这种处于NAT之后的端对端的直连建立之后,连接的双方可以轮流担任 对方的媒人,把对方介绍给其他的客户端,这样就极大的降低了服务器S的工作量;第二,应用程序不用关心这个NAT是属于cone还是symmetric,即便要,如果连接的双方有一方或者双方都恰好不处于NAT之后,基于上叙的步骤,他们之间还是可以建立很好的通信通道;第三,打洞技术能够自动运作在多重NAT之后,不论连接的双方经过多少层NAT才到达Internet,都可以进行通信。

 

 

译后小记:本来已经翻译好了,是在网文快捕中翻译的,结果,一个全选把所有翻译的内容全部删除了(网文快捕的Bug?:),不得不痛苦的再翻一遍。不过,有失必有得,第二次翻译流畅多了,希望大家读来还顺口。

 

3.3.2. Peers behind the same NAT  客户端都处于相同的NAT之后

 

Now consider the scenario in which the two clients (probably unknowingly) happen to reside behind the same NAT, and are therefore located in the same private IP address space.  Client A has established a UDP session with server S, to which the common NAT has assigned public port number 62000.  Client B has similarly established a session with S, to which the NAT has assigned public port number 62001.

 

现在让我们来考虑一下两个客户端(很有可能不知不觉的就会)同时位于相同的NAT之后,而且是在同一个子网内部的情况, Client AS之间的会话使用了NAT62000端口,Client BS之间的会话使用了62001端口,如下图所示:

      

        Suppose that A and B use the UDP hole punching technique as outlined above to establish a communication channel using server S as an introducer.  Then A and B will learn each other’s public IP addresses and port numbers as observed by server S, and start sending each other messages at those public addresses.The two clients will be able to communicate with each other this way as long as the NAT allows hosts on the internal network to open translated UDP sessions with other internal hosts and not just with external hosts. We refer to this situation as "loopback translation," because packets arriving at the NAT from the private network are translated and then "looped back" to the private network rather than being passed through to the public network.  For example, when A sends a UDP packet to B’s public address, the packet initially has a source IP address and port number of 10.0.0.1:124 and a destination of 155.99.25.11:62001.  The NAT receives this packet, translates it to have a source of  155.99.25.11:62000 (A’s public address) and a destination of 10.1.1.3:1234, and then forwards it on to B.  Even if loopback translation is supported by the NAT, this translation and forwarding   step is obviously unnecessary in this situation, and is likely to add latency to the dialog between A and B as well as burdening the NAT.

  

我们假设,Client A Client B 要使用上一节我们所描述的 UDP打洞技术”,并通过服务器S这个“媒人”来认识,这样Client A Client B首先从服务端S得到了彼此的公网IP地址和端口,然后就往对方的公网IP地址和端口上发送消息。在这种情况下,如果NAT 仅仅允许在 内部网主机与其他内部网主机(处于同一个NAT之后的网络主机)之间打开UDP会话通信通道,而内部网主机与其他外部网主机就不允许的话,那么Client A Client B就可以通话了。我们把这种情形叫做“loopback translation(“回环转换”),因为数据包首先从局域网的私有IP发送到NAT转换,然后“绕一圈”,再回到局域网中来,但是这样总比这些数据通过公网传送好。举例来说,当 Client A发送了一个UDP数据包到 Client B的公网IP地址,这个数据包的报头中就会有一个源地址10.0.0.1:124和一个目标地址155.99.25.11:62001NAT接收到这个包以后,就会(进行地址转换)解析出这个包中有一个公网地址源地址155.99.25.11:62000和一个目标地址10.1.1.3:1234,然后再发送给B,虽说NAT支持“loopback translation”,我们也发现,在这种情形下,这个解析和发送的过程有些多余,并且这个Client A Client B 之间的对话可能潜在性地给NAT增加了负担。

 

The solution to this problem is straightforward, however. When A and B initially exchange address information through server S, they should include their own IP addresses and port numbers as "observed" by themselves, as well as their addresses as observed by S.The clients    then simultaneously start sending packets to each other at each of the alternative addresses they know about, and use the first address that leads to successful communication. If the two clients are behind the same NAT, then the packets directed to their private addresses are likely to arrive first, resulting in a direct communication channel not involving the NAT.  If the two clients are behind different NATs, then the packets directed to their private addresses will fail to reach each other at all, but the clients will hopefully establish connectivity using their respective public addresses. It is important that these packets be authenticated in some way, however, since in the case of different NATs it is entirely possible for A’s messages directed at B’s private address to reach some other, unrelated node on A’s private network, or vice versa.

 

其实,解决这个问题的方案是显而易见的。当 Client AClientB 最初通过服务器S交换彼此的地址信息时,他们也就应该“发现”了自己的IP地址和端口——也就是服务器S所发现的。两个客户端同时的发送 数据包 到对方的公网地址和私有地址上,然后选择首先使得通信成功的那个地址就可以了。如果两个客户端都位于同一个NAT之后,那么发往私有地址的数据包应该先于发往公网地址的数据包到达,这样就建立了一个不包括NAT的直连通信通道。如果两个客户端位于不同NAT之后,虽然发送到对方私有地址的数据包会毫无疑问的发送失败,但还是很有可能使用他们各自的公网IP地址来建立一条通信通道的。所以检测这些数据包的方法和工作就变得非常重要,不论如何,只要双方都处于不同NAT之后,就完全有可能 Client A 想发送到 Client B 的信息会被发到别的无关的地方去,反之亦然(Client B 想发送到 Client A的消息也会被发到别的无关的地方去)。

 

(最后一句“unrelated node on A’s private network”没有完全理解是什么意思,总之,放到整个语境中,应该就是说,Client A 瞄准 Client B的私有地址端口的信息会被NAT转发到别的地方去,因为两者处于不同的NAT之后,NAT A 如果在 内部网络 找到了一个拥有与Client B相同的私有地址的电脑,就会把信息发送过去,这样,就根本不会发送到 Client B 上去)

3.3.3. Peers separated by multiple NATs 客户端分别处于多层NAT之后

 

        In some topologies involving multiple NAT devices, it is not possible for two clients to establish an "optimal" P2P route between them without specific knowledge of the topology.  Consider for example the following situation.

 

在有些网络拓扑中就存在多层NAT设备,如果不熟悉网络拓扑的知识,要想建立一条“理想的”端对端连接基本上是不可能的。让我们来看看下图这种情况:


       Suppose NAT X is a large industrial NAT deployed by an internet service provider (ISP) to multiplex many customers onto a few public IP addresses, and NATs A and B are small consumer NAT gateways deployed independently by two of the ISP’s customers to multiplex their private home networks onto their respective ISP-provided IP addresses. Only server S and NAT X have globally routable IP addresses; the "public" IP addresses used by NAT A and NAT B are actually private to the ISP’s addressing realm, while client A’s and B’s addresses in turn are private to the addressing realms of NAT A and B, respectively.

Each client initiates an outgoing connection to server S as before, causing NATs A and B each to create a single public/private translation, and causing NAT X to establish a public/private translation for each session.

 

假如 NAT X 是由 Internet服务供应商(ISP 配置的一个 大型工业 NAT,它使用少量的公网IP地址来为一些客户群提供服务;NAT A NAT B 则是为ISP的两个客户群所配置的小一点的独立NAT网关,它们为各自客户群的私人家庭网络提供IP地址。只有 Server S NAT X 拥有 公网固定IP地址,而NAT A NAT B所拥有的“公网”IP地址对于ISP的寻址域来说则实际上“私有”的,这时 Client A的地址对于NAT A的寻址领域来说是“私有”的,Client B的地址对于NAT B的寻址域来说同样是“私有”的。

还是跟以前一样,每个客户端都建立了一个“外出”的连接到服务器S,导致NATA NAT B 分别进行一次 公有/私有 转换,并导致 NAT X 每个 会话都建立了一个 公有/私有 的转换。(也就是把私有地址转换成为公网地址的过程,NAT的本质工作)

  

Now suppose clients A and B attempt to establish a direct peer-to- peer UDP connection.  The optimal method would be for client A to send messages to client B’s public address at NAT B,   192.168.1.2:31000 in the ISP’s addressing realm, and for client B to send messages to A’s public address at NAT B, namely 192.168.1.1:30000.  Unfortunately, A and B have no way to learn these addresses, because server S only sees the "global" public addresses of the clients, 155.99.25.11:62000 and 155.99.25.11:62001.Even if A and B had some way to learn these addresses, there is still no guarantee that they would be usable because the address assignments in the ISP’s private addressing realm might conflict with unrelated address assignments in the clients’ private realms. The clients therefore have no choice but to use their global public addresses as seen by S for their P2P communication, and rely on NAT X to provide   loopback translation.

 

现在让我们假设 Client A Client B 想要建立一条 端对端 UDP 直连。理想的方法应该是 Client A 发送一条 信息到 Client B NAT B的公网地址192.168.1.2:31000,这个地址在ISP的寻址域内;同时 Client B也发送一条消息到Client A NAT B的公网地址上,也就是192.168.1.1:30000;如果能这样发的话,问题就解决了。可惜Client A Client B根本就不可能知道对方的这个地址,因为Server S只记录了他们真正的公网地址155.99.25.11:62000155.99.25.11:62001。即使 Client A Client B 通过某种途径得知了这些地址,还是不能够保证这样就能进行通话了,因为这些地址是由ISP的私有寻址域分配的,可能会与私有域所分配的其他无关客户端地址相冲突因此,如果客户端之间想要进行端对端的通信的话,别无选择,只能通过他们真正的公网地址来进行;并且 NAT X必须还得支持 loopback translation”才行。

 

3.3.4. Consistent port bindings  保持端口绑定

 

The hole punching technique has one main caveat: it works only if both NATs are cone NATs (or non-NAT firewalls), which maintain a consistent port binding between a given (private IP, private UDP) pair and a (public IP, public UDP) pair for as long as that UDP port is in use.  Assigning a new public port for each new session, as a symmetric NAT does, makes it impossible for a UDP application to reuse an already-established translation for communication with   different external destinations.  Since cone NATs are the most widespread, the UDP hole punching technique is fairly broadly applicable; nevertheless a substantial fraction of deployed NATs are symmetric and do not support the technique.

 

在使用“UDP打洞技术”时有一点必须要注意:它只能在双方的NAT都是cone NAT(或者干脆没有NAT)时才能正常工作;这些NAT在自己的公网UDP端口被使用时保持着端口的绑定——[私有IP,私有UDP端口]对和[公网IP,公网UDP端口]对的一一对应。如果像 symmetricNAT那样给每个新的会话都分配一个新的公网端口,那么UDP应用程序想要与其他外部客户端进行通话,就无法重复使用已经建立好的通信转换。

伴随着 cone NAT 的推广,“UDP打洞技术”也被越来越广泛的应用。然而,仍存在一小部分使用 symmetric NAT 的网络,那么在这小部分网络环境中,就不能使用“UDP打洞技术”。

 

(注:因为我国的国情,网络技术应用得比较晚,所以可以说绝大部分的网络都是cone NAT,所以 UDP打洞技术基本上可以畅通无阻的使用,只是还要注意对NAT是否支持“loopback translation”的测试)

3.3.2. Peers behind the same NAT  客户端都处于相同的NAT之后

 

Now consider the scenario in which the two clients (probably unknowingly) happen to reside behind the same NAT, and are therefore located in the same private IP address space.  Client A has established a UDP session with server S, to which the common NAT has assigned public port number 62000.  Client B has similarly established a session with S, to which the NAT has assigned public port number 62001.

 

现在让我们来考虑一下两个客户端(很有可能不知不觉的就会)同时位于相同的NAT之后,而且是在同一个子网内部的情况, Client AS之间的会话使用了NAT62000端口,Client BS之间的会话使用了62001端口,如下图所示:

      

        Suppose that A and B use the UDP hole punching technique as outlined above to establish a communication channel using server S as an introducer.  Then A and B will learn each other’s public IP addresses and port numbers as observed by server S, and start sending each other messages at those public addresses.The two clients will be able to communicate with each other this way as long as the NAT allows hosts on the internal network to open translated UDP sessions with other internal hosts and not just with external hosts. We refer to this situation as "loopback translation," because packets arriving at the NAT from the private network are translated and then "looped back" to the private network rather than being passed through to the public network.  For example, when A sends a UDP packet to B’s public address, the packet initially has a source IP address and port number of 10.0.0.1:124 and a destination of 155.99.25.11:62001.  The NAT receives this packet, translates it to have a source of  155.99.25.11:62000 (A’s public address) and a destination of 10.1.1.3:1234, and then forwards it on to B.  Even if loopback translation is supported by the NAT, this translation and forwarding   step is obviously unnecessary in this situation, and is likely to add latency to the dialog between A and B as well as burdening the NAT.

  

我们假设,Client A Client B 要使用上一节我们所描述的 UDP打洞技术”,并通过服务器S这个“媒人”来认识,这样Client A Client B首先从服务端S得到了彼此的公网IP地址和端口,然后就往对方的公网IP地址和端口上发送消息。在这种情况下,如果NAT 仅仅允许在 内部网主机与其他内部网主机(处于同一个NAT之后的网络主机)之间打开UDP会话通信通道,而内部网主机与其他外部网主机就不允许的话,那么Client A Client B就可以通话了。我们把这种情形叫做“loopback translation(“回环转换”),因为数据包首先从局域网的私有IP发送到NAT转换,然后“绕一圈”,再回到局域网中来,但是这样总比这些数据通过公网传送好。举例来说,当 Client A发送了一个UDP数据包到 Client B的公网IP地址,这个数据包的报头中就会有一个源地址10.0.0.1:124和一个目标地址155.99.25.11:62001NAT接收到这个包以后,就会(进行地址转换)解析出这个包中有一个公网地址源地址155.99.25.11:62000和一个目标地址10.1.1.3:1234,然后再发送给B,虽说NAT支持“loopback translation”,我们也发现,在这种情形下,这个解析和发送的过程有些多余,并且这个Client A Client B 之间的对话可能潜在性地给NAT增加了负担。

 

The solution to this problem is straightforward, however. When A and B initially exchange address information through server S, they should include their own IP addresses and port numbers as "observed" by themselves, as well as their addresses as observed by S.The clients    then simultaneously start sending packets to each other at each of the alternative addresses they know about, and use the first address that leads to successful communication. If the two clients are behind the same NAT, then the packets directed to their private addresses are likely to arrive first, resulting in a direct communication channel not involving the NAT.  If the two clients are behind different NATs, then the packets directed to their private addresses will fail to reach each other at all, but the clients will hopefully establish connectivity using their respective public addresses. It is important that these packets be authenticated in some way, however, since in the case of different NATs it is entirely possible for A’s messages directed at B’s private address to reach some other, unrelated node on A’s private network, or vice versa.

 

其实,解决这个问题的方案是显而易见的。当 Client AClientB 最初通过服务器S交换彼此的地址信息时,他们也就应该“发现”了自己的IP地址和端口——也就是服务器S所发现的。两个客户端同时的发送 数据包 到对方的公网地址和私有地址上,然后选择首先使得通信成功的那个地址就可以了。如果两个客户端都位于同一个NAT之后,那么发往私有地址的数据包应该先于发往公网地址的数据包到达,这样就建立了一个不包括NAT的直连通信通道。如果两个客户端位于不同NAT之后,虽然发送到对方私有地址的数据包会毫无疑问的发送失败,但还是很有可能使用他们各自的公网IP地址来建立一条通信通道的。所以检测这些数据包的方法和工作就变得非常重要,不论如何,只要双方都处于不同NAT之后,就完全有可能 Client A 想发送到 Client B 的信息会被发到别的无关的地方去,反之亦然(Client B 想发送到 Client A的消息也会被发到别的无关的地方去)。

 

(最后一句“unrelated node on A’s private network”没有完全理解是什么意思,总之,放到整个语境中,应该就是说,Client A 瞄准 Client B的私有地址端口的信息会被NAT转发到别的地方去,因为两者处于不同的NAT之后,NAT A 如果在 内部网络 找到了一个拥有与Client B相同的私有地址的电脑,就会把信息发送过去,这样,就根本不会发送到 Client B 上去)

3.3.3. Peers separated by multiple NATs 客户端分别处于多层NAT之后

 

        In some topologies involving multiple NAT devices, it is not possible for two clients to establish an "optimal" P2P route between them without specific knowledge of the topology.  Consider for example the following situation.

 

在有些网络拓扑中就存在多层NAT设备,如果不熟悉网络拓扑的知识,要想建立一条“理想的”端对端连接基本上是不可能的。让我们来看看下图这种情况:


       Suppose NAT X is a large industrial NAT deployed by an internet service provider (ISP) to multiplex many customers onto a few public IP addresses, and NATs A and B are small consumer NAT gateways deployed independently by two of the ISP’s customers to multiplex their private home networks onto their respective ISP-provided IP addresses. Only server S and NAT X have globally routable IP addresses; the "public" IP addresses used by NAT A and NAT B are actually private to the ISP’s addressing realm, while client A’s and B’s addresses in turn are private to the addressing realms of NAT A and B, respectively.

Each client initiates an outgoing connection to server S as before, causing NATs A and B each to create a single public/private translation, and causing NAT X to establish a public/private translation for each session.

 

假如 NAT X 是由 Internet服务供应商(ISP 配置的一个 大型工业 NAT,它使用少量的公网IP地址来为一些客户群提供服务;NAT A NAT B 则是为ISP的两个客户群所配置的小一点的独立NAT网关,它们为各自客户群的私人家庭网络提供IP地址。只有 Server S NAT X 拥有 公网固定IP地址,而NAT A NAT B所拥有的“公网”IP地址对于ISP的寻址域来说则实际上“私有”的,这时 Client A的地址对于NAT A的寻址领域来说是“私有”的,Client B的地址对于NAT B的寻址域来说同样是“私有”的。

还是跟以前一样,每个客户端都建立了一个“外出”的连接到服务器S,导致NATA NAT B 分别进行一次 公有/私有 转换,并导致 NAT X 每个 会话都建立了一个 公有/私有 的转换。(也就是把私有地址转换成为公网地址的过程,NAT的本质工作)

  

Now suppose clients A and B attempt to establish a direct peer-to- peer UDP connection.  The optimal method would be for client A to send messages to client B’s public address at NAT B,   192.168.1.2:31000 in the ISP’s addressing realm, and for client B to send messages to A’s public address at NAT B, namely 192.168.1.1:30000.  Unfortunately, A and B have no way to learn these addresses, because server S only sees the "global" public addresses of the clients, 155.99.25.11:62000 and 155.99.25.11:62001.Even if A and B had some way to learn these addresses, there is still no guarantee that they would be usable because the address assignments in the ISP’s private addressing realm might conflict with unrelated address assignments in the clients’ private realms. The clients therefore have no choice but to use their global public addresses as seen by S for their P2P communication, and rely on NAT X to provide   loopback translation.

 

现在让我们假设 Client A Client B 想要建立一条 端对端 UDP 直连。理想的方法应该是 Client A 发送一条 信息到 Client B NAT B的公网地址192.168.1.2:31000,这个地址在ISP的寻址域内;同时 Client B也发送一条消息到Client A NAT B的公网地址上,也就是192.168.1.1:30000;如果能这样发的话,问题就解决了。可惜Client A Client B根本就不可能知道对方的这个地址,因为Server S只记录了他们真正的公网地址155.99.25.11:62000155.99.25.11:62001。即使 Client A Client B 通过某种途径得知了这些地址,还是不能够保证这样就能进行通话了,因为这些地址是由ISP的私有寻址域分配的,可能会与私有域所分配的其他无关客户端地址相冲突因此,如果客户端之间想要进行端对端的通信的话,别无选择,只能通过他们真正的公网地址来进行;并且 NAT X必须还得支持 loopback translation”才行。

 

3.3.4. Consistent port bindings  保持端口绑定

 

The hole punching technique has one main caveat: it works only if both NATs are cone NATs (or non-NAT firewalls), which maintain a consistent port binding between a given (private IP, private UDP) pair and a (public IP, public UDP) pair for as long as that UDP port is in use.  Assigning a new public port for each new session, as a symmetric NAT does, makes it impossible for a UDP application to reuse an already-established translation for communication with   different external destinations.  Since cone NATs are the most widespread, the UDP hole punching technique is fairly broadly applicable; nevertheless a substantial fraction of deployed NATs are symmetric and do not support the technique.

 

在使用“UDP打洞技术”时有一点必须要注意:它只能在双方的NAT都是cone NAT(或者干脆没有NAT)时才能正常工作;这些NAT在自己的公网UDP端口被使用时保持着端口的绑定——[私有IP,私有UDP端口]对和[公网IP,公网UDP端口]对的一一对应。如果像 symmetricNAT那样给每个新的会话都分配一个新的公网端口,那么UDP应用程序想要与其他外部客户端进行通话,就无法重复使用已经建立好的通信转换。

伴随着 cone NAT 的推广,“UDP打洞技术”也被越来越广泛的应用。然而,仍存在一小部分使用 symmetric NAT 的网络,那么在这小部分网络环境中,就不能使用“UDP打洞技术”。

 

(注:因为我国的国情,网络技术应用得比较晚,所以可以说绝大部分的网络都是cone NAT,所以 UDP打洞技术基本上可以畅通无阻的使用,只是还要注意对NAT是否支持“loopback translation”的测试)

随着互联网的发展,网络资源越加丰富,怎样才能共享网络资源,发挥互联网的作用?这个时候有一种叫P2P的技术随之而诞生。这项技术可以让用户之间直接进行文件交换,没有任何的中间环节,这种技术就是P2P技术。P2P技术的出现与发展,让人们重新发现了一种全新的文件交换方式。

走进P2P

所谓P2P,就是消费者和生产者之间为达到一定的目的而进行的直接的、双向的信息或服务的交换。在这里P2P即是英文Peer to Peer的简称,其中Peer是“同等的人、伙伴”的意思。国内的媒体一般将P2P翻译成“端对端”或者“点对点”,P2P的实质即代表了信息和服务在一个个人或对等设备与另一个个人与对等设备间的流动。

我们可以通过以下几个关键词,进一步了解上面的定义:

关键词一:行为。P2P所涉及的是一种交换行为,这种行为是实时动态的,而非静止的。

关键词二:双向交换。P2P不是单向的,它的价值就在于交换。与Web中付费换取产品或服务的交换不同的是,P2P扩大了交换的方式和范围,每个参与交换的用户都可能同时成为产品的生产者和消费者,整个网络更像一个物品丰富的繁荣的集市,令每个进入P2P网络的人都受益。

关键词三:信息。提供信息的方式和信息的质量是P2P概念中的又一亮点。在P2P中,所有的信息都是由对等的个人计算机系统创建的,这些信息将存放在用户自己的计算机内,而无需像Web下“发布”在某个其他的地方。在Web站点上发布信息存在延时和困难,这些都令信息本身的价值大打折扣,而P2P则依靠自己的资源,无需在服务器上分配空间,这样就可以提供实时的可升级的信息。

关键词四:服务。P2P下单个的对等设备提供的服务是Web环境下很难实现的。Web站点服务器往往承担着相当集中并且繁忙的工作,而典型的用户个人计算机更多涉及的是屏幕保护程序,大量的资源和服务被闲置。试想过众多对等设备同时提供高级的应用软件,共同参与一项沉重的运算作业吗?P2P就能够帮助实现这一点。

关键词五:直接性。在网上,P2P的用户可以享受最直接最迅速的交换活动,他们不必再忍受位置等级和格式转换,因为P2P中没有中介。

关键词六:生产者和消费者。与Web站点大不同的是,生产者创建信息和服务并允许用户访问和使用这些内容,P2P下,每台个人计算机或对等设备既是消费者,又是生产者,任何对等设备都可以提供信息和服务,同时消费信息和服务。

关键词七:目标。目标在P2P中的地位超过了网络,信息和服务必须传输给确定的目标用户,这种传输是有目标的,而非盲目的。

这就是P2P。它以用户为中心,所有的用户都是平等的伙伴。相隔万里的用户可以通过P2P共享硬盘上的文件、目录乃至整个硬盘。所有人都共享了他们认为最有价值的东西,这将使互联网上信息的价值得到极大的提升。这种用户间直接交流的方式,真正实现了互联网共享和自由的梦想,它改变了互联网现有的游戏规则,也改变了我们的生活。

P2P的应用三部曲

搜寻引擎,挑战Google

I5 Digital 公司所开发的搜寻引擎 Pandango则运用了P2P 网络架构特性,把搜寻引擎技术带入更高的层次:Pandango 以检索辐射状的network of referrer,用户下载 Pandango 后再输入欲搜寻的关键词,就可以和 100 名 referrer 组成的网络联机,然后进入他们的计算机搜寻他们的上网历史与标示的书签,再透过这 100 人的计算机和另外 10000 名 referrer 的计算机串连,再去进行搜寻。也就是说,每一次的搜寻就可以涵盖 100 万笔相关的数据。

不仅如此,Pandango 的设计还可以让网友依时间与搜寻主题更改 referrer名单的排序,较常被搜寻的 referrer 就会被排在比较前面,藉由此一方式筛选出和自己比较MATCH的 100 名referrer 名单,搜寻的相符性当然就大大提高了。

企业应用,前景可期

至于在企业应用方面,P2P虽是新技术,但有些人认为网络传输档案等功能就是这种概念的执行,举凡像美国在线(AOL)的实时信差、ICQ及微软(Microsoft)MSN信差服务,最成功的莫过于Napster及SETI@Home了。HP、IBM及Intel等世界大厂并共同组成了P2P工作小组联盟,共同推动P2P运算标准及通讯协议的制定,其它参与厂商尚有初创公司Entropia、Distributed Sciences及Popular Power。

另外值得一提的是,英特尔将P2P概念,推动P2P运算标准及通讯协议,掀起第三波因特网革命。怎么说呢?第一波网络革命可说是动态撷取以文字为主的内容,第二代网络革命则是以多媒体网站为平台的网络,第三波网际网络革命则始于Napster 、Gnutella、Freenet及英特尔所力推的P2P运算架构。

英特尔技术长帕特吉辛格宣布推出的马奎计划(Marquam program),主要就是希望可以扩大P2P的应用,希望激发P2P的基础建设,来整合业界资源,方可结合业界投入P2P科技研发,并且加强营销的方式,使P2P深入一般消费市场及企业用户,进而创造及提升客户端的新需求。

相关的市场人士就表示,因为企业对于计算器资源的需求是无限大的,且P2P网络拥有降低成本及提高生产力的潜力,所以P2P运算技术普及,反而需要更多的客户端计算器资源与控管P2P运算网络的服务器系统,对信息产业的发展将具有正面的帮助。所以企业早晚会运用P2P术,不过是时间及应用程度的问题罢了!

电子商务 引人注目

除上述之外,P2P在电子商务上的应用也十分引人注目。主要表现在

金融服务:由于P2P的沟通只单纯涉及沟通双方,不会有第三者知道双方沟通的讯息,所以P2P非常适合发展在线金融服务。美国的Billpoint已将P2P技术应用在电子商务的付费机制,在在线拍卖网站eBay当中,就以这种技术提供全球35个国家的使用者,可以直接用彼此的信用卡做交易。

购物行为分析:而P2P 的合作过滤(collaborative filtering)功能也帮助商务网站如:Amazon就常借用合作过滤功能来分析网友的购物行为,然后据此揣测他们的好恶并推荐适合他们的商品。

电子商务市集:利用P2P把庞大的档案互换社群转化为另类的电子商务市集,这家公司称为Lightshare,他将推出一种服务,让个别计算机使用者直接透过计算机销售数字产品,而不用经由eBay或亚马逊(Amazon.com)的中央服务器。这种服务脱胎自eBay,转化为点对点(point-to-point)模式。任何交换的信息其实都不存在我们的计算机内,我们做的只是加速数据交换过程而已。Lightshare将以全球信息网为营运据点,让任何想买卖数字产品者透过Lightshare的网站交易,此技术是直接透过买卖双方的计算机进行交换,就像用Napster互换音乐文件不必经由Napster公司的服务器一般。但产品刚开始可能只是歌曲或软件等数字档案而已,日后可能扩及脚踏车等实物。Lightshare并不会对一般消费者之间的销售抽取佣金,而将以协助中小企业建置电子商店,透过此档案互换网络成交者再收费。不过因为担心诈欺和盗版的问题,所以Lightshare表示,会针对安全性顾虑改善软件技术,防范黑客入侵、计算机病毒等后遗症,并拦阻使用者透过Lightshare服务贩卖盗版档案。

广告营销的应用:透过P2P应用程序可了解用户对信息类型的偏好,是良好的客户信息收集系统,透过P2P让广告商首次可以挖掘到消费者对电影、音乐、软件等任何可以交换的数字档案的偏好,所以其广告效力高于标题式广告或电子邮件,因为锁定对象的准确极高,而广告商则依据消费者回应的次数付费。另一个IM广告商Big Champagne则表示,实时传讯服务带来可观的广告商机,例如:P2P的好处在于所有人都用匿名,所以广告商简直是如入无人之境,可以肆意的窥探用户交流的内容、电影、音乐、学校作业等,然后再将这些宝贵的资料研究消费者的好恶与未来的趋势,当然是市场研究中的至宝。

P2P广告载具:能配合客户信息其广告效益非常高,对有意以网络为营销通路的企业大有帮助。美国几家大型网络广告公司,积极透过P2P挖掘潜在消费者的喜好,然后透过IM向他们拉广告,说服他们购买哪位艺人的专辑或造访哪位艺人的网页。

网络电话:知名的网络电话(IP telephony)团体利用P2P技术来驾驭PC 网络,为打造出P2P的网络架构,让大众透过网络和彼此的电话线,把打国际电话变成打市内电话,IP telephony提供免费电话的服务,消费者只要花费 $ 150 美元购买设备即可。这类利用把点对点技术扩大到让消费者间彼此分享「通讯网络」。Forrester Research 公司分析师表示:「这项方案是可怕的梦魇。如果电讯业者参与其中,或许还有机会成功,….但不能背着电讯业者偷偷地做。」 其实,这项P2P网络电话系统是瞄准宽带使用者为服务对象,使用者可透过网络路由器和思科的「网关器」,把数码电话讯号转成网络流量后,再传送到受话者的普通电话。

国内P2P市场的特点

目前国内虽然有十余家P2P公司进入市场竞争的行列,但无论从产品水平还是用户规模上,参与竞争的企业可以说是实力相当。根据计算机世界网2004年4月19日的一篇文稿提供的即时数据显示,在该文稿截稿之时,PP点点通累计注册用户1300万,同时在线2万人;OP(Openext)累计注册用户也在1400万以上,活跃用户230万左右,自2003年11月开始收费后,同时在线保持在2.3万人左右;另一家较小规模的Reallink由武汉维宇软件公司运营,已经有450万用户,同时在线也已过万人大关。还有另一个不容忽视的情况是,这几家公司的规模都在30人以下。

收费与免费之战:在当前国内的P2P市场,围绕付费问题展开的竞争仍然很激烈。十几种P2P产品中收费产品和免费产品几乎各占半壁江山。免费产品:POCO 、KuGoo(酷狗)、 ezPeer(易载) 、百宝、QQMessenger、KDT个人版;收费产品:Kuro、Openext、PP点点通、QQ、KDT企业版。

竞争焦点相对集中:和国外P2P市场的竞争态势有所的不同,国内P2P市场的竞争焦点相对集中,无论公司实力还是用户规模,即时通信类产品与文件共享交换类产品,都占据明显的优势。竞争焦点的集中加速了同类产品的优胜劣汰,对这两类P2P产品整体水平的提高都起到了重要的作用。同时,竞争焦点的集中有为后来加入P2P市场的成员创造了机遇,在P2P的其他应用领域都具有广阔的发展空间。

竞争的结局难以预测:目前,国内P2P产品普遍处于发展竞争的初期,正如前面所说,参与竞争的企业实力相当,因此,这样的自由竞争格局将会延续。然而,人们不禁会想,这样的格局正蕴藏的巨大的机遇,此时,如果有大量资金投入到某个P2P服务提供商,迅速壮大规模,说不定可以将国内的P2P产业一统江湖。谁会结束这一切?目前恐怕难以找到答案。

可以说,今日中国的P2P产品市场是特色鲜明的,它刚起步,因此还有太多的领域蕴藏商机;它还年轻,因此无论是产品还是竞争都显得不够成熟。在中国,围绕P2P更多的商业应用和有效的盈利模式,还有太多事要做。对于即将投入国内P2P市场的企业来说,这一切既是机遇也是挑战。

点评:从目前的状况来分析,中国的P2P市场正处在自由竞争阶段,进入市场的企业无论是规模还是实力,都不相上下。因此,在较长的一段时期,中国的P2P市场将延续群雄逐鹿的竞争局面。互联网实验室认为,当前P2P行业标准尚未形成,而P2P的相关应用又及其广泛,技术门槛与经营门槛都相对较低,是这种竞争局面长期存在的主要原因。在尝试了P2P在即时通信、音乐共享与下载等方面的产品开发与经营后,P2P在中国的发展将会以探求P2P更多的商业应用为核心。在这方面,国外许多P2P企业就走在了前面。无论是协同办公,还是分布式计算,国外的P2P企业都有过比较成功的探索,为国内P2P企业提供了许多值得借鉴的经验。而国内立足于这两方面应用的P2P企业,数量很少,规模也远不及国外的竞争对手。

 

P2P对于用户最大的意义,不是它的技术和功能,而是它的理念。这种理念源于人们互联网的憧憬和梦想,它使网络回归到Internet的本质,让共享与自由的精神充满网络世界。中国P2P的发展,必将经历一个从技术到理念的过渡,技术的不断进步为中国P2P的发展铺平道路,而理念的不断更新,则为中国P2P的发展指明方向。未来的竞争,不再仅仅以技术为导向,谁能以P2P的理念创造为网民所接受和留恋的产品和文化,谁才会最终夺得P2P胜利之杯。 

微软亚洲研究院网络多媒体组 吴枫 李世鹏


  一、 流媒体系统及其发展趋势


  所谓流媒体是指用户通过网络或者特定数字信道边下载边播放多媒体数据的一种工作方式。流媒体应用的一个最大的好处是用户不需要花费很长时间将多媒体数据全部下载到本地后才能播放,而仅需将起始几秒的数据先下载到本地的缓冲区中就可以开始播放,后面收到的数据会源源不断输入到该缓冲区,从而维持播放的连续性,因此流媒体播放器通常只是在开始时有一些时延。流媒体系统要比下载播放系统复杂得多,所以需要将多媒体的编解码和传输技术很好地结合在一起,才能确保用户在复杂的网络环境下也能得到较稳定的播放质量。

  多媒体数据在传输前必须要先经过编码器有效地压缩成码流,以减少对网络资源的占用率。目前常用的视频编码器有MPEG-2、MPEG-4、H.261、H.263、H.264、Window Media视频编码器和Real System视频编码器等;音频编码器有MP3、MPEG AAC、Window Media 音频编码器和AMR等;图像编码器有JPEG和JPEG2000等。多媒体编码器所生成的码流只包含了解码该码流所必需的信息,它不包含媒体间的同步、随机访问等系统信息,因此编码后的多媒体数据还要被组织成为具有特定系统格式的多媒体文件用于流媒体传输或者是存入磁盘中,目前常用的文件格式有MPEG-2系统,MP4,微软公司的ASF,Real的文件格式,QuickTime的文件格式以及用于3G无线服务的3GPP和3GPP2等等。

  当流媒体在实时应用中(如现场流媒体广播),根据当前的网络状况和用户的终端参数,多媒体数据是一边被编码一边被流媒体服务器传输给用户。而在其他的非实时应用中,多媒体数据可以被事先编码生成多媒体文件,存储在磁盘阵列中。当提供多媒体服务时,流媒体服务器直接读取这些文件传输给用户,这样服务方式对设备的要求较低。目前许多流媒体服务属于后一种方式,这样就要求流媒体服务器具有一定的机制来适应网络状况和用户设备。

  目前码流自适应这一模块主要采用的方法有:将多媒体文件中的视频码流转换为一个特定码率和图像尺寸的码流;或者把同一段视频内容编码生成多个具有不同码率和图像尺寸的码流,然后自适应选择一个最合适的码流传输给用户。生成的码流还需要进一步打包成为特定网络传输协议的数据包用于网络传输,由于现在许多网络并不能保证传输的数据能够及时并完全正确地被用户收到,传输的数据包可能需要加前向纠错编码(FEC)来保护,经过这些处理后多媒体数据就可以通过网络传输给用户,目前常用的传输协议有RTP/RTCP、HTTP和MMS。

  用户收到传输的数据后,如果存在丢包或者是比特出错,错误恢复处理会根据附加的纠错数据来恢复传输错误。如果还不能恢复传输错误,用户端可以向服务器发出重传请求,在解码开始前重新传输丢失的包。恢复后的多媒体数据将由解码器解码得到重构的多媒体数据,由于容错保护和数据重传可能不能恢复所有的错误数据,错误掩藏模块可以利用重构的多媒体数据的相关性来掩盖这些错误,最后这些数据就播放给用户。

  通常流媒体系统中的服务器和用户间并不是单向通信,如前面提到的重传请求。事实上,用户端会传递给服务器许多反馈信息,如终端设备的能力和网络连接速度会传给服务器的码流自适应模块来调整码流,在实时应用中这些信息还可能传给编码器;用户端的丢包率、数据包收到的时间信息和用户缓冲区状态等信息也会传递给服务器来估计当前的网络状况,从而控制码流的自适应和数据的发送策略。从上面的描述来看,实际上流媒体系统在多媒体信息处理中是一个非常复杂的系统,目前市面上主要的产品有微软公司的Windows Media, Real公司的Real System和苹果公司的QuickTime,其中Windows Media系统的市场占有率最大。

  这篇文章主要集中讨论流媒体的发展趋势和出现的新的服务和技术。早期的流媒体系统常用在互联网上传输一些低质量的多媒体信息,但是随着网络技术的发展,一些高质量的流媒体应用已经开始出现,如IPTV将向用户传输标清甚至高清的电视节目。另外,随着无线网络和各种各样手持设备的出现,无线流媒体的应用也变得越来越重要。并且由于很多现代家庭中既有高端的PC和电视,又有多种功能的手机,PDA,便携式媒体播放器,流媒体也将在家庭娱乐和数据共享上一显身手。针对这些应用的需求,流媒体技术本身也在迅速地变革和发展,例如利用一些高效的编码技术和传输技术提高流媒体系统性能;发展新的标准扩展流媒体技术到各种不同的网络和设备;在流媒体系统中增加更多的新功能来满足应用的需要。

  二、 流媒体新服务

  本章将讨论流媒体系统的一些新的应用和服务。

  1. IPTV

  据国际电信联盟ITU在2004年9月的一份报告指出,全球的宽带用户已经在去年底首度突破 1亿大关,其中中国电信的宽带用户就超过了1千万,用户的主要接入方式是ADSL和以太网线,其实际的连接速率可以达到1Mbit/s。而且随着高性能的编码技术的采用,如H.264和最新的Windows Media视频编码器,800kbit/s的视频流就可以接近或达到DVD质量。

  在这种情况下,扩展流媒体技术用来提供电视服务也就顺理成章了。IPTV,也叫交互式网络电视,就是利用流媒体技术通过宽带网络传输数字电视信号给用户,这种应用有效地将电视、电讯和PC三个领域结合在一起,具有很强的发展前景。IPTV可以采用两种不同的方式提供用户电视服务,组播或者广播方式和视频点播(VOD)方式。一个明显的优势是IPTV是基于现在互联网的方式来实现服务器和用户终端的连接,因此很容易同时提供现有的互联网的服务,将电视服务和互联网浏览,电子邮件,以及多种在线信息咨询、娱乐、教育及商务功能结合在一起。

  2. 无线流媒体

  2.5G、3G以及超3G无线网络的发展也使得流媒体技术可以被用到无线终端设备上,目前中国联通公司提供CDMA 1x,用户网络带宽最多可以达到100kbit/s,这已经足够提供QCIF大小的流媒体服务;而且随着3G无线网络的应用,用户的网络带宽可以达到384kbit/s。另一方面,手机设备运算能力越来越强,存储空间越来越大,不用说SMART Phone和Pocket PC等高端手机,就是一般的中档手机,如Nokia 6610,也能实现基本的H.264的软件解码。

  面向无线网络的流媒体应用对当前的编码和传输技术提出了更大的挑战,首先,相对于有线网络而言,无线网络状况更不稳定,除去网络流量所造成的传输速率的波动外,手持设备的移动速度和所在位置也会严重地影响到传输速率,因此高效的可自适应的编码技术至关重要。其次,无线信道的环境也要比有线信道恶劣的多,数据的误码率也要高许多,而高压缩的码流对传输错误非常敏感,还会造成错误向后面的图像扩散,因此无线流媒体在信源和信道编码上需要很好的容错技术。尽管手机设备的运算能力越来越强,但是由于它是由电池供电的,因此编解码处理不能太复杂,并且最好能够根据用户设备的电池来调整流媒体的接收和处理,能源管理技术也是移动流媒体的一个研究热点。

  3. 电子家庭

  现代家庭中的越来越多的设备可以用来采集,接收,发送和播放多媒体数据。如人们可以通过电视来收看电视节目,通过PC机在互联网上欣赏流媒体节目,通过自己的数字相机和摄像机来拍摄图像和视频,通过手机和其他手持设备来发送彩信,通过汽车的音响系统来欣赏音乐和广播。并且家庭中的网络连接也是多样化的,如电视连接有线电视网,PC机连接着互联网,手机连接着无线网络,而且这些设备也能在家里通过蓝牙或者802.11无线网连接在一起。

  所有这些设备所收到的多媒体数据如何在家庭网络和设备间共享,为流媒体的发展提供了一个更大的舞台,真正实现一种无所不在、随心所至的多媒体服务,让多媒体真正地像液体一样自由流动起来。流媒体在家庭网络应用中的关键是如何使多媒体数据能够适应不同的设备的能力,如在电视和PC机中播放的视频的大小可能是标清甚至是高清,但是同样的内容就可能需要经过流媒体系统有效的转换才能成为最适合在手持设备上播放的媒体。

  三、 流媒体新技术

  这章我们将讨论高效的视频编码、可伸缩的视频编码和P2P技术,它们都能极大地改进当前流媒体系统的性能。

  1. 高效的编码技术

  流媒体系统中的多媒体数据要通过网络来传输给用户,高效的编码技术可以极大地降低流媒体系统对网络带宽的要求。目前标准化和商业化的视频编码技术都是基于运动补偿和DCT变换的,从早期的MPEG-1和H.261,到最新的MPEG-4 AVC/H.264和Windows Media视频编码器都采用了这个框架。在这个框架中,运动估计和补偿模块用来消除相邻图像间的冗余信息,熵编码模块用来消除编码信号的冗余性,变换量化模块根据人的视觉系统对视频信号的细微变化的不敏感性丢失部分信息,从而提高压缩比。

  在这个编码框架下,过去十多年的时间内编码技术取得了很大的发展,事实上,最新MPEG-4 AVC/H.264标准的编码效率要比MPEG-1提高了4倍左右,除去更精细的运动补偿和基于上下文的熵编码外,帧内预测,多参考帧的预测,环路滤波和率失真优化技术也极大地提高了该标准的性能。

  2. 可伸缩性编码技术

  在前面两章中我们也讨论过,在流媒体应用中需要解决的一个基本问题是网络带宽的波动,不同的人在不同的时刻使用互联网和无线网络时,得到的数据传输率存在着很大的差异;甚至同一个人在同一个时刻,哪怕是在传输同一个视频流,实际的数据传输率也会存在较大的波动。目前在流媒体系统中所用的编码技术都是生成固定码率的码流,它们很难适应如此复杂的网络带宽的波动。一个有效的方法是采用可伸缩性的视频编码,MPEG-4和H.263标准中就包含了分层的可伸缩性的视频编码,它们提供一定的适应网络带宽变化的能力,但是在流媒体应用中人们更期望视频编码技术能提供精细的码流可伸缩性,MPEG-4 FGS就是一种这样的编码技术,目前MPEG-21可伸缩视频编码组正在研究两套编码方案:高效的FGS编码方案和3D小波编码方案。

  3. 多媒体标准技术

  多媒体编码标准在流媒体里是至关重要的。一方面标准的制定和执行确保不同厂家和服务商之间可以互通互联,另一方面标准里的知识产权也是商家必争之处。掌握了标准里的知识产权,在竞争的时候就有很大的主动权。所以很多商家乃至政府部门都在全力推出自己的知识产权到各种国际标准里去,甚至打造自己的产业或国家标准。

  4. 对等网络技术(P2P)

  P2P是当前互联网上较热门的技术,已应用到网络文件共享和Napster的MP3下载。其基本思想是通过P2P技术,除了和服务器外,每个用户可以共享他的文件或信息给其他用户。

  P2P技术也可以应用到流媒体,每个流媒体用户也是一个P2P中的一个节点,在目前的流媒体系统中用户之间是没有任何联系的,但是采用P2P技术后,用户可以根据他们的网络状态和设备能力与一个或几个用户建立连接来分享数据,这种连接能减少服务器的负担和提高每个用户的视频质量。P2P技术在流媒体应用中特别适用于一些热门事件,即使是大量的用户同时访问流媒体服务器,也不会造成服务器因负载过重而瘫痪。此外,对于多人的多媒体实时通信,P2P技术也会对网络状况和音视频质量带来很大改进。

  P2P技术如果与可伸缩性视频编码技术结合将能极大地提高每个用户所接收的视频质量。由于可伸缩性码流的可加性,媒体数据不用全部传输给每个用户,而是把它们分散传输给每个用户,再通过用户间的连接,每个用户就可以得到合在一起的媒体数据。即使每个用户与服务器的连接带宽是有限的,应用P2P技术,每个用户依然可以通过流媒体系统享受高质量的多媒体服务。

  四、结束语

  流媒体的发展正处在一个酝酿着突变的阶段。无论从应用、服务和技术,都将会产生一系列重大的突破。在流媒体的领域里,重点不应是只放在几个孤立的关键技术上,而是应该把流媒体当作一个系统工程,编码、传输、分享、网络以及设备都是互相联系的一个整体。怎么能在这样一个系统里,最有效地将流媒体以一种最适合用户终端设备的形式传送给用户,并且不增加服务器和网络负担,可能是能否在流媒体领域的竞争中立于不败之地的根本。

1.      技术
1.1     P2P
1.1.2   P2P现有系统

这些现有系统包括了
eMule  
MLDonkey  
aMule  
Bittorrent  
Kademlia/Overnet clients  
Shareaza  
FastTrack clients  
Kazaa Lite  
iMesh  
Grokster  
WinMX  
Gnutella clients  
Soulseek  
Freenet  
ShareDaemon  
RevConnect  
Gnucleus  
eFarm  
DC++  
pDonkey  
Piolet  
Blubster  
RockitNet  
Waste

名称 说明
eMule  
MLDonkey  
aMule  
Bittorrent  
Kademlia/Overnet clients  
Shareaza  
FastTrack clients  
Kazaa Lite  
iMesh  
Grokster  
WinMX  
Gnutella clients  
Soulseek  
Freenet  
ShareDaemon  
RevConnect  
Gnucleus  
eFarm  
DC++  
pDonkey  
Piolet  
Blubster  
RockitNet  
Waste  


1.1.1    P2P网络模型
1.1.1.1    静态配置模型



静态配置模型是一种相对静态而简单的对等点定位模型。在该模型中,每个对等点都确切地知道存在于其P2P 网络中其它对等点的位置以及它们所提供的共享资源内容。

缺点:网络无法应付不能预知的随机事件和临时变更,比如对等点随机进入和退出网络。

优点:整个网络在外部攻击面前表现得很稳固。

1.1.1.2   动态配置模型(目录式)

在目录式模型中,一台或多台有特殊用途的服务器为对等点提供目录服务。对等点向目录服务注册关于自身的信息(其名称、地址、资源和元数据),并通过根据目录服务器中信息的查询,使用目录服务来定位其它对等点。Napster模型是一种典型的使用动态配置模型(目录式)的网络模型

缺点:网络的不安全性(服务器失效则该服务器下的对等点全部失效),成本问题。

优点:提高了网络的可管理性,使得对共享资源的查找和更新非常方便。

1.1.1.3  动态配置模型(网络式)

它由许多对等点组成,这些对等点在功能上很类似。没有专门的目录服务器。对等点必须使用它们所在的网络来定位其它对等点。没有一个对等点知道整个网络的结构或者组成网络的每个对等点的身份。希望知道网络中另一个对等点的位置时,它就发出一个查询请求并传递给邻居。这些邻居尝试满足这个请求。如果这些邻居不能完全满足这个请求,就将请求传递给它们的邻居,以此类推。Gnutella模型是一种典型的使用动态配置模型(网络式)的网络模型

缺点:容易导致网络拥塞,对大型网络应用并不适合。

优点:在查询过程中具有较大的灵活性。

1.1.1.4  动态配置模型(多播式)

除了网络中的节点不必协助发现以外,多播模型和网络模型很相似。这种模型利用网络自身提供的特性来定位和确认对等点和资源。对等点使用IP多播技术定期宣布自己的存在,对此消息感兴趣的对等点检测这个消息后,抽取出主机名和端口号,并使用这个信息与新对等点建立正常的 TCP/IP 连接。

缺点:众多子网间的路由多播通信是一个非常复杂的课题;因特网对多播并不友好。

优点:减少网络流量不会因对等组中任何一个对等点的瘫痪而崩溃。

1.1.1.5  动态配置模型(散列式)

不需要专门的服务器,网络中所有的对等点都是服务器,并且承担很小的服务器的功能。首先将网络中的每一个节点分配虚拟地址(VID),同时用一个关键字(KEY)来表示其可提供的共享内容。取一个散列函数,这个函数可以将KEY转换成一个散列值H(KEY)。网络中节点相邻的定义是散列值相邻。发布信息的时候就把(KEY,VID)二元组发布到具有和H(KEY)相近地址的节点上去,其中VID指出了文档的存储位置。资源定位的时候,就可以快速根据H(KEY)到相近的节点上获取二元组(KEY, VID),从而获得文档的存储位置。

缺点:

优点:

VoIP:电话通信进化历史 最热门的VoIP莫过于Skype



  1995年以色列VocalTec公司所推出的Internet Phone,不但是VoIP网络电话的滥觞,并揭开了电信IP化的序幕。人们从此不但可以享受到更便宜、甚至完全免费的通话及多媒体加值服务,电信业的服务内容及面貌也为之剧变。

  一开始的网络电话是以软件的形式呈现,同时仅限于PC to PC间的通话,换句话说,人们只要分别在两端不同的PC上,安装网络电话软件,即可经由IP网际路进行对话。随著宽频普及与相关网络技术的演进,网络电话也由单纯PC to PC的通话形式,发展出IP to PSTN、PSTN to IP、PSTN to PSTN及IP to IP等各种形式,当然他们的共通点,就是以IP网络为传输媒介,如此一来,电信业长久以PSTN电路交换网网络为传输媒介的惯例及独占性也逐渐被打破。

  VoIP的原理、架构及要求

  由Voice over IP的字面意义,可以直译为透过IP网络传输的语音讯号或影像讯号,所以VoIP就是一种可以在IP网络上互传类比音讯或视讯的一种技术。简单地说,它是藉由一连串的转码、编码、压缩、打包等程序,好让该语音资料可以在IP网络上传输到目的端,然后再经由相反的程序,还原成原来的语音讯号以供接听者接收。

  进一步来说,VoIP大致透过5道程序来互传语音讯号,首先是将发话端的类比语音讯号进行编码的动作,目前主要是采用ITU-T G.711语音编码标准来转换。第二道程序则是将语音封包加以压缩,同时并添加址及控制资讯,如此便可以在第三阶段中,也就是传输IP封包阶段,在浩瀚的IP网络中寻找到传送的目的端。到了目的端,IP封包会进行解码还原的作业,最后并转换成喇叭、听筒或耳机能播放的类比音讯。

  在一个基本的VoIP架构之中,大致包含4个基本元素:

  (1)媒体闸道器(Media Gateway):主要扮演将语音讯号转换成为IP封包的角色。

  (2)媒体闸道控制器(Media Gateway Controller):又称为Gate Keeper或Call Server。主要负责管理讯号传输与转换的工作。

  (3)语音伺服器:主要提供电话不通、占线或忙线时的语音回应服务。

  (4)信号闸道器(Signaling Gateway):主要工作仍在交换过程中进行相关控制,以决定通话建立与否,以及提供相关应用的加值服务。

  虽然VoIP拥有许多优点,但绝不可能在短期内完全取代已有悠久历史并发展成熟的PSTN电路交换网,所以现阶段两者势必会共存一段时间。为了要让两者间能相互沟通,势必要建立一个互通的介面及管道,而媒体闸道器与闸道管理器即扮演了中介的色角,因为他们具备将媒体资料流及IP封包转译成不同网络所支援的各类协定。

  其运作原理是,媒体闸道器先将语音转换为IP封包,然后交由媒体闸道控制器加以控制管理,并决定IP封包在网络中的传送路径。至于信号闸道器则负责将SS7信号格式转换为IP封包。

  网络电话若要走向符合企业级营运标准,必须达到以下几个基本要求:

  1.服务品质(QoS)之保证:这是由PSTN过渡到VoIP、IP PBX取代PBX的最基本要求。所谓QoS就是要保证达到语音传输的最低延迟率(400毫秒)及封包遗失率(5-8%),如此通话品质才能达到现今PSTN的基本要求及水准,否则VoIP的推行将成问题。

  2.99.9999%的高可用性(High Available;HA):虽然网络电话已成今后的必然趋势,但与发展已久的PSTN相较,其成熟度、稳定度、可用性、可管理性,乃至可扩充性等方面,仍有待加强。尤其在电信级的高可用性上,VoIP必须像现今PSTN一样,达到6个9(99.9999%)的基本标准。目前VoIP是以负载平衡、路由备份等技术来解决这方面的要求及问题,总而言之,HA是VoIP必须达到的目标之一。

  3.开放性及相容性:传统PSTN是属封闭式架构,但IP网络则属开放式架构,如今VoIP的最大课题之一就是如何在开放架构下,而能达到各家厂商VoIP产品或建设的互通与相容,同时地造成各家产品在整合测试及验证上的困难度。目前的解决方法是透过国际电信组织不断拟定及修改的标准协定,来达到不同产品间的相容性问题,以及IP电话与传统电话的互通性。

  4.可管理性与安全性问题:电信服务包罗万象,包括用户管理、异地漫游、可靠计费系统、认证授权等等,所以管理上非常复杂,VoIP营运商必须要有良好的管理工具及设备才能因应。同时IP网络架构技术完全不同于过去的PSTN电路网,而且长久以来具开放性的IP网络一直有著极其严重的安全性问题,所以这也形成网络电话今后发展上的重大障碍与首要解决的目标。

  5.多媒体应用:与传统PSTN相比,网络电话今后发展上的最大特色及区别,恐怕就在多媒体的应用上。在可预见的未来,VoIP将可提供互动式电子商务、呼叫中心、企业传真、多媒体视讯会议、智慧代理等应用及服务。过去,VoIP因为价格低廉而受到欢迎及注目,但多媒体应用才是VoIP今后蓬勃发展的最大促因,也是各家积极参与的最大动力。

  主宰VoIP走向的三大主流协定

  在浩瀚的IP网络中要如何正确的寻找到要通话的对方并建立对答,同时也能依照彼此资料的处理能力来传送语音资料,这中间必须藉由国际电信组织所拟定的标准协定才能达到。如今,市面上的网络电话大致都会遵循H.323、MGCP及SIP等3种标准协定。虽然目前产品仍以支援H.323为多,但SIP的支援将会成为今后主流。

  1.H.323

  ITU-T 国际电联第16研究组首先在1996年通过H.323第一版的制定工作,同时并在1998年完成第二版协定的拟定。原则上,该协定提供了基础网络(Packet Based Networks;PBN)架构上的多媒体通讯系统标准,并为IP网络上的多媒体通讯应用提供了技术基础。

  H.323并不依赖于网络结构,而是独立于作业系统和硬体平台之上,支援多点功能、组播和频宽管理。H.323具备相当的灵活性,可支持包含不同功能节点之间的视讯会议和不同网络之间的视讯会议。

  H.323并不支援群播(Multicast)协定,只能采用多点控制单元(MCU)构成多点会议,因而同时只能支持有限的多点用户。H.323也不支持呼叫转移,且建立呼叫的时间也比较长。

  早期的视讯会议多半支援H.323协定,例如微软NetMeeting、Intel Internet Video Phone等都是支援H.323协定的视讯会议软件,亦为现今VoIP的前辈。

  不过H.323协定本身具有一些问题,例如采用H.323协定的IP电话网络在接入端仍要经过当地的PSTN电路交换网。而之后制定出的MGCP等协定,目的即在于将H.323闸道进行功能上的分解,也就是划分成负责媒体流处理的媒体闸道(MG),以及掌控呼叫建立与控制的媒体闸道控制器(MGC)两个部分。

  虽然如今微软的Windows Mesenger则已改采SIP标准,且SIP标准隐隐具有取代H.323的势头。但目前仍有许多网络电话产品依旧支援H.323协定。

  2.SIP(Session Initiation Protocol)

  SIP是由IETF所制定,其特性几乎与H.323相反,原则上它是一种比较简单的会话初始化协定,也就是只提供会话或呼叫的建立与控制功能。SIP协定可支援多媒体会议、远端教学及Internet电话等领域的应用。

  SIP同时支援单点播送(Unicast)及群播功能,换句话说,使用者可以随时加入一个已存在的视讯会议之中。在网络OSI属性上,SIP属于应用层协议,所以可透过UDP或TCP协定进行传输。

  SIP另一个重要特点就是它属于一种基于文本的协定,采用SIP规则资源定位语言描述(SIP Uniform Resource Locators),因此可方便地进行撰改或测试作业,所以比起H.323来说,其灵活性与扩展性的表现较好。

  SIP的URL甚至可以嵌入到Web页面或其他超文本连结之中,用户只需用滑鼠一点即可发出呼叫。所以与H.323相比,SIP具备了快速建立呼叫快与支援电话号码之传送等特点。

  3.MGCP

  原则上,MGCP协定与前两者皆不同,H.323和SIP协定是专门针对网络电话及IP网络所提出的两套各自独立的标准,两者间并不相容及互通。反观MGCP协定,则与IP电话网络无关,而只牵涉到闸道分解上的问题,也因为如此,该协定可同时适用于支援H.323或SIP协定的网络电话系统。

  MGCP协定制定的主要目的即在于将闸道功能分解成负责媒体流处理的媒体闸道(MG),以及掌控呼叫建立与控制的媒体闸道控制器(MGC)两大部分。同时MG在MGC的控制下,实现跨网域的多媒体电信业务。

  由于MGCP更加适应需要中央控管的通讯服务模式,因此更符合电信营运商的需求。在大规模网络电话网中,集中控管是件非常重要的事情,透过MGCP则可利用MGC统一处理分发不同的服务给MG。

  4.其他重要协定及技术

  除了上述3大协定之外,还有许多左右VoIP通话品质及传输效率的重要协定与技术。在语音压缩编码技术方面,主要有ITU-T定义的G.729、G.723等技术,其中G.729提供了将原有64Kbit/s PSTN类比语音,压缩到只有8Kbit/s,而同时符合不失真需求的能力。

  在即时传输技术方面,目前网络电话主要支援RTP传输协定。RTP协定是一种能提供端点间语音资料即时传送的一种标准。该协定的主要工作在于提供时间标签和不同资料流程同步化控制作业,收话端可以藉由RTP重组发话端的语音资料。除此之外,在网络传输方面,尚包括了TCP、UDP、闸道互联、路由选择、网络管理、安全认证及计费等相关技术。

     
 拟定组机  ITU-T IETF  IETF 
 架构 P2P   P2P  主从式
 设计对象 ISDN及ATM   Internet Gateway 
 QoS 无  有  N/A 
 复杂度 高  低   N/A
 扩充度 低  高  中 
 延伸性 中  高 
 编码 二进位编码  基于文本·类似HTTP     N/A

VoIP 三大协定比较

  VoIP各项产品及设备的类型

  和许多早期网络设备一样,VoIP最早是以软件的形态问世的,也就是纯粹PC to PC功能的产品。为了能贴近过去传统类比电话的使用习惯及经验,之后才渐渐有电话形态的产品出现。对于企业而言,为了追求成本、语音及网络的整合、多媒体加值功能、更方便的集中式管理,而陆续出现了VoIP闸道、IP PBX或其他整合型的VoIP设备等解决方案。以下就这几种类型的VoIP产品做一简单介绍。

  1.VoIP软件

  VoIP软件不但是网络电话的原始形态,更是开启免费通话新世纪到来的开路先锋。对于熟悉电脑及网络操作的人而言,只要发收双方电脑上安装VoIP软件,即可穿越网际网络相互通话,这实在是件既神奇又方便的事。更重要的是,透过VoIP软件,不论是当地PC to PC的对话,抑或跨国交谈都几乎免费,同时网上并有许多免费的VoIP软件提供下载,也因为如此,.VoIP才能紧紧锁住一般消费者乃至企业用户的目光。

  但对于绝大多数的使用者而言,必须克服电脑软件安装及操作的门槛,还要安插耳机及麦克风,更要面对系统不稳定或当机的可能性,所以透过PC来打电话不但是件麻烦事,而且是一种与既有通话习惯不符的奇怪行径。

  不论如何,VoIP软件背后所潜藏的无限商机,不但吸收了许多人的目光,同时成为VoIP兵家必争的焦点。从早期的视讯会议软件,到即时通讯软件,再到今日造成风潮的Skype都是明显的例子。不论是Wintel阵营中的微软、Intel,抑或Yahoo、AOL、Google、PC Home Online等入口网站、甚或ISP厂商,全都卯足全劲进行各种抢摊作业。

  其中,许多ISP并推出整合VoIP软件及USB话机的销售方案,例如SEEDNet的Wagaly Walk及PC Home Online的“PChome Touch-1”USB话机。至于外型上与一般电话无异的USB话机,就是为营造出一种与传统电话外观及使用习性相同的一种解决方案。

  目前市面上最热门的VoIP软件莫过于Skype,该软件表示由于采用P2P技术,所以可以绕过伺服器与防火墙的拦截,所以能在传输效能、话音及服务品质等方面皆有不错的表现。

  Skype大致分成可供免费下载的Skype,以及需要购买点数、可用于PC拨打至市话、手机、国际电话的Skype-out,以及从市话、手机拨打电话至Skype电脑的Skype-in。

  Skype网络电话软件

  2.VoIP网络电话

  一般而言VoIP网络电话的又分成有线、无线VoIP网络电话,以及提供影像输出的VoIP视讯会议设备等不同类型的产品。由于VoIP网络电话机上具备RJ45网络介面埠,所以不需藉由电脑主机,即可透过宽频、连结IP网络进行通话,同时使用习性上与传统电话一样,一般人很难分辨出其中的差异。

  VoIP网络电话较少用于个人家庭或SOHO市场,但却何做为企业VoIP网络建设中的终端设备。但由于目前VoIP网络电话的价格仍高,所以仍不普遍,虽然之前,拜SARS之赐,促进了些VoIP视讯会议设备的销售业绩,但整体而言成长幅并不太大。

  思科网络电话7900系列IP Phone

  例如由合勤科技推出的Prestige 2000W,即为一款VoIP无线网络电话机,透过802.11b/g的AP即可连上IP网络并与彼端的使用者通话。

  此外,微软特别在Win CE 5.0中新增VoIP功能,除了强化与Exchange Server的整合性外,并提供讯息整合及身份管理等功能,届时藉由WinCE开发出来的VoIP电话,将提供多人共用,但每人皆有私人专属帐号的功能。

  3.VoIP闸道器

  除了VoIP软件之外,VoIP闸道器可说是最普遍常见的网络电话设备。不论是家用或商用领域中,VoIP闸道器可说扮演了由传统PSTN网络转输到IP网络的介面,换句话说,透过它即可用传统的电话设备(乃至PBX交换系统)来打网络电话。

  岱升Sky Dailer VoIP闸道器。

  拜宽频普及之赐,几乎家家户户的电脑旁都安装有所谓的ADSL数据机,而市面并有许多针对宽频的AP装置即为一种闸道配备,目前内含频宽分享、无线网络、防火墙、入侵侦测等功能的整合性的闸道配备可说红透半边天,为了同时搭上整合及VoIP风潮的列车,ISP业者遂推出VoIP闸道器的相关服务方案。例如SEEDNet的Wagaly Talk、亚太线上的iCall gateway。此外,原本的网络设备商当然也不会放过跨入VoIP领域的机会,不论是网络巨擘思科,抑或国内的岱升、全景、康全、宏远电信、零壹或合勤科技等公司都有相应的产品推出。

  思科AS5800 Series Universal Gateways

  4.VoIP PBX

  在电信级的网络电话架构中,IP PBX语音交换机扮演了相当重要的角色,其不但需要接手传统语音交换机的位置及功能,还要居中成为语音与资讯整合的媒介。IP PBX功能强大且多样化,能透过Web-Based介面提供使用者一个简单容易的操作环境。

  依据思科常久在IP PBX的经验指出,在IP电话网络架构中,IP PBX是一个可促使语音流量顺利传至所指定终端的设备。IP电话将语音讯号转换为IP封包后,由IP PBX透过讯号控制决定其封包的传输方向。当此通电话终点为一般电话时,其IP PBX便将IP封包送至VoIP闸道器,然后由VoIP闸道器转换IP封包,再回传到一般TDM的PSTN电路交换网。

  总之,对于企业而言,IP PBX具备降低基础建设成本、减低管理成本及增加工作效率之综合运用、降低转移至IP电话系统的风险等优点。

2005年07月20日

在如今的网络应用中,文件的传送是重要的功能之一,也是共享的基础。一些重要的协议像HTTP,FTP等都支持文件的传送。尤其是FTP,它的全称就是“文件传送协议”,当 初的工程师设计这一协议就是为了解决网络间的文件传送问题,而且以其稳定,高速,简单而一直保持着很大的生命力。作为一个程序员,使用这些现有的协议传送文件相当简单,不过,它们只适用于服务器模式中。这样,当我们想在点与点之间传送文件就不适用了或相当麻烦,有一种大刀小用的意味。笔者一直想寻求一种简单有效,且具备多线程断点续传的方法来实现点与点之间的文件传送问题,经过大量的翻阅资料与测试,终于实现了,现把它共享出来,与大家分享。
我写了一个以此为基础的实用程序(网络传圣,包含源代码),可用了基于TCP/IP的电脑上,供大家学习。
upload/2004_06/04062118541204.gif

(本文源代码运行效果图)


实现方法(VC++,基于TCP/IP协议)如下:
仍釆用服务器与客户模式,需分别对其设计与编程。
服务器端较简单,主要就是加入待传文件,监听客户,和传送文件。而那些断点续传的功能,以及文件的管理都放在客户端上。

一、服务器端

首先介绍服务器端:
最开始我们要定义一个简单的协议,也就是定义一个服务器端与客户端听得懂的语言。而为了把问题简化,我就让服务器只要听懂两句话,一就是客户说“我要读文件信息”,二就是“我准备好了,可以传文件了”。
由于要实现多线程,必须把功能独立出来,且包装成线程,首先建一个监听线程,主要负责接入客户,并启动另一个客户线程。我用VC++实现如下:

DWORD WINAPI listenthread(LPVOID lpparam)
{ 

    //由主函数传来的套接字
  SOCKET  pthis=(SOCKET)lpparam;
    //开始监听
 int rc=listen(pthis,30);
    //如果错就显示信息
    if(rc<0){
   CString aaa;
   aaa="listen错误\n";
      AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.Get
Buffer(0),1); aaa.ReleaseBuffer(); return 0; } //进入循环,并接收到来的套接字 while(1){ //新建一个套接字,用于客户端 SOCKET s1; s1=accept(pthis,NULL,NULL);   //给主函数发有人联入消息 CString aa; aa="一人联入!\n"; AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aa.GetBuff
er(0),1); aa.ReleaseBuffer(); DWORD dwthread; //建立用户线程 ::CreateThread(NULL,0,clientthread,(LPVOID)s1,0,&dwthread); } return 0; }

接着我们来看用户线程:
先看文件消息类定义

struct fileinfo
{
 int fileno;//文件号
 int type;//客户端想说什么(前面那两句话,用1,2表示)
 long len;//文件长度
 int seek;//文件开始位置,用于多线程

 char name[100];//文件名
};

用户线程函数:

DWORD WINAPI clientthread(LPVOID lpparam)
{
 //文件消息
 fileinfo* fiinfo;
 //接收缓存
 char* m_buf;
 m_buf=new char[100];
 //监听函数传来的用户套接字
 SOCKET  pthis=(SOCKET)lpparam;
 //读传来的信息
 int aa=readn(pthis,m_buf,100);
 //如果有错就返回
 if(aa<0){
  closesocket (pthis);
  return -1;
 }
 //把传来的信息转为定义的文件信息
 fiinfo=(fileinfo*)m_buf;
 CString aaa;
 //检验客户想说什么
 switch(fiinfo->type)
 {
 //我要读文件信息
 case 0:
 //读文件
 aa=sendn(pthis,(char*)zmfile,1080);
 //有错
 if(aa<0){
  closesocket (pthis);
  return -1;
 }
 //发消息给主函数
 aaa="收到LIST命令\n";
     AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBu
ffer(0),1); break; //我准备好了,可以传文件了 case 2: //发文件消息给主函数 aaa.Format("%s 文件被请求!%s\n",zmfile[fiinfo->fileno].name,nameph[fii
nfo->fileno]); AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer
(0),1); //读文件,并传送 readfile(pthis,fiinfo->seek,fiinfo->len,fiinfo->fileno); //听不懂你说什么 default: aaa="接收协议错误!\n"; AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBu
ffer(0),1); break; } return 0; }

读文件函数

void readfile(SOCKET  so,int seek,int len,int fino)
{
 //文件名
 CString myname;
 myname.Format("%s",nameph[fino]);
 CFile myFile;
 //打开文件
 myFile.Open(myname, CFile::modeRead | CFile::typeBinary|CFile::shareDen
yNone); //传到指定位置  myFile.Seek(seek,CFile::begin); char m_buf[SIZE]; int len2; int len1; len1=len; //开始接收,直到发完整个文件 while(len1>0){ len2=len>SIZE?SIZE:len; myFile.Read(m_buf, len2); int aa=sendn(so,m_buf,len2); if(aa<0){ closesocket (so); break; } len1=len1-aa; len=len-aa; } myFile.Close(); }

服务器端最要的功能各技术就是这些,下面介绍客户端。

二、客户端

客户端最重要,也最复杂,它负责线程的管理,进度的记录等工作。

大概流程如下:
先连接服务器,接着发送命令1(给我文件信息),其中包括文件长度,名字等,然后根据长度决定分几个线程下载,并初使化下载进程,接着发送命令2(可以给我传文件了),并记录文件进程。最后,收尾。
这其中有一个十分重要的类,就是cdownload类,定义如下:

class cdownload
{
public:
 void createthread();//开线程
 DWORD finish1();//完成线程
 int sendlist();//发命令1
 downinfo doinfo;//文件信息(与服务器定义一样)
 int startask(int n);开始传文件n
 long m_index;
 BOOL good[BLACK];
 int  filerange[100];
 CString fname;
 CString fnametwo;
 UINT threadfunc(long index);//下载进程

 int sendrequest(int n);//发文件信息
 cdownload(int thno1);
 virtual ~cdownload();
};

下面先介绍sendrequest(int n),在开始前,向服务器发获得文件消息命令,以便让客户端知道有哪些文件可传

int cdownload::sendrequest(int n)
{
 //建套接字
 sockaddr_in local;
 SOCKET m_socket;

 int rc=0;
 //初使化服务器地址
 local.sin_family=AF_INET;
 local.sin_port=htons(1028);
 local.sin_addr.S_un.S_addr=inet_addr(ip);
 m_socket=socket(AF_INET,SOCK_STREAM,0);

 int ret;
 //联接服务器
 ret=connect(m_socket,(LPSOCKADDR)&local,sizeof(local));
 //有错的话
 if(ret<0){
  AfxMessageBox("联接错误");
 closesocket(m_socket);
 return -1;
 }
 //初使化命令
 fileinfo fileinfo1;
 fileinfo1.len=n;
 fileinfo1.seek=50;
 fileinfo1.type=1;
 //发送命令
 int aa=sendn(m_socket,(char*)&fileinfo1,100);
 if(aa<0){
  closesocket(m_socket);
  return -1;
 }
 //接收服务器传来的信息
  aa=readn(m_socket,(char*)&fileinfo1,100);
 if(aa<0){
  closesocket(m_socket);
  return -1;
 }
 //关闭
 shutdown(m_socket,2);
 closesocket(m_socket);

 return 1;
}

有了文件消息后我们就可以下载文件了。在主函数中,用法如下:

//下载第clno个文件,并为它建一个新cdownload类
down[clno]=new cdownload(clno);
//开始下载,并初使化
type=down[clno]->startask(clno);
//建立各线程
createthread(clno);

下面介绍开始方法:

//开始方法
int cdownload::startask(int n)
{
 //读入文件长度
 doinfo.filelen=zmfile[n].length;
 //读入名字
 fname=zmfile[n].name;
 CString tmep;
 //初使化文件名
 tmep.Format("\\temp\\%s",fname);

 //给主函数发消息
 CString aaa;
 aaa="正在读取 "+fname+" 信息,马上开始下载。。。\n";
 AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer
(0),1); aaa.ReleaseBuffer(); //如果文件长度小于0就返回 if(doinfo.filelen<=0) return -1; //建一个以.down结尾的文件记录文件信息 CString m_temp; m_temp=fname+".down"; doinfo.name=m_temp; FILE* fp=NULL; CFile myfile; //如果是第一次下载文件,初使化各记录文件 if((fp=fopen(m_temp,"r"))==NULL){ filerange[0]=0; //文件分块 for(int i=0;i<BLACK;i++) { if(i>0) filerange[i*2]=i*(doinfo.filelen/BLACK+1); filerange[i*2+1]=doinfo.filelen/BLACK+1; } filerange[BLACK*2-1]=doinfo.filelen-filerange[BLACK*2-2]; myfile.Open(m_temp,CFile::modeCreate|CFile::modeWrite | CFile::typeBina
ry); //写入文件长度 myfile.Write(&doinfo.filelen,sizeof(int)); myfile.Close();   CString temp; for(int ii=0;ii<BLACK;ii++){ //初使化各进程记录文件信息(以.downN结尾) temp.Format(".down%d",ii); m_temp=fname+temp; myfile.Open(m_temp,CFile::modeCreate|CFile::modeWrite | CFile::typeBina
ry); //写入各进程文件信息 myfile.Write(&filerange[ii*2],sizeof(int)); myfile.Write(&filerange[ii*2+1],sizeof(int)); myfile.Close(); } ((CMainFrame*)::AfxGetMainWnd())->m_work.m_ListCtrl->AddItemtwo(n,2,0,0
,0,doinfo.threadno); } else{ //如果文件已存在,说明是续传,读上次信息 CString temp;   m_temp=fname+".down0"; if((fp=fopen(m_temp,"r"))==NULL) return 1; else fclose(fp); int bb; bb=0; //读各进程记录的信息 for(int ii=0;ii<BLACK;ii++) { temp.Format(".down%d",ii); m_temp=fname+temp;   myfile.Open(m_temp,CFile::modeRead | CFile::typeBinary); myfile.Read(&filerange[ii*2],sizeof(int)); myfile.Read(&filerange[ii*2+1],sizeof(int)); myfile.Close(); bb = bb+filerange[ii*2+1]; CString temp; } if(bb==0) return 1; doinfo.totle=doinfo.filelen-bb;   ((CMainFrame*)::AfxGetMainWnd())->m_work.m_ListCtrl->AddItemtwo(n,2,doi
nfo.totle,1,0,doinfo.threadno); }   //建立下载结束进程timethread,以管现各进程结束时间。 DWORD dwthread; ::CreateThread(NULL,0,timethread,(LPVOID)this,0,&dwthread); return 0; }

下面介绍建立各进程函数,很简单:

void CMainFrame::createthread(int threadno)
{
 DWORD dwthread;
 //建立BLACK个进程
 for(int i=0;i<BLACK;i++)
 {
  m_thread[threadno][i]= ::CreateThread(NULL,0,downthread,(LPVOID)down[t
hreadno],0,&dwthread); } }

downthread进程函数

DWORD WINAPI downthread(LPVOID lpparam)
{
 cdownload* pthis=(cdownload*)lpparam;
 //进程引索+1
 InterlockedIncrement(&pthis->m_index);
 //执行下载进程
 pthis->threadfunc(pthis->m_index-1);
 return 1;
}

下面介绍下载进程函数,最最核心的东西了

UINT cdownload::threadfunc(long index)
{
 //初使化联接
 sockaddr_in local;
 SOCKET m_socket;

 int rc=0;
 
 local.sin_family=AF_INET;
 local.sin_port=htons(1028);
 local.sin_addr.S_un.S_addr=inet_addr(ip);
 m_socket=socket(AF_INET,SOCK_STREAM,0);

 int ret;
 //读入缓存
 char* m_buf=new char[SIZE];
 int re,len2;
 fileinfo fileinfo1;
 //联接
 ret=connect(m_socket,(LPSOCKADDR)&local,sizeof(local));
 //读入各进程的下载信息
 fileinfo1.len=filerange[index*2+1];
 fileinfo1.seek=filerange[index*2];
 fileinfo1.type=2;
 fileinfo1.fileno=doinfo.threadno;
 
 re=fileinfo1.len;
 
 //打开文件 
 CFile destFile;
 FILE* fp=NULL;
 //是第一次传的话
 if((fp=fopen(fname,"r"))==NULL)
  destFile.Open(fname, CFile::modeCreate|CFile::modeWrite | CFile::typeB
inary|CFile::shareDenyNone); else //如果文件存在,是续传 destFile.Open(fname,CFile::modeWrite | CFile::typeBinary|CFile::shareD
enyNone); //文件指针移到指定位置 destFile.Seek(filerange[index*2],CFile::begin); //发消息给服务器,可以传文件了 sendn(m_socket,(char*)&fileinfo1,100); CFile myfile; CString temp; temp.Format(".down%d",index); m_temp=fname+temp;   //当各段长度还不为0时 while(re>0){ len2=re>SIZE?SIZE:re;   //读各段内容 int len1=readn(m_socket,m_buf,len2); //有错的话 if(len1<0){ closesocket(m_socket); break; }   //写入文件 destFile.Write(m_buf, len1); //更改记录进度信息 filerange[index*2+1]-=len1; filerange[index*2]+=len1; //移动记录文件指针到头 myfile.Seek(0,CFile::begin); //写入记录进度 myfile.Write(&filerange[index*2],sizeof(int)); myfile.Write(&filerange[index*2+1],sizeof(int)); //减去这次读的长度 re=re-len1; //加文件长度 doinfo.totle=doinfo.totle+len1; }; //这块下载完成,收尾   myfile.Close(); destFile.Close(); delete [] m_buf; shutdown(m_socket,2);     if(re<=0) good[index]=TRUE; return 1; }

到这客户端的主要模块和机制已基本介绍完。希望好好体会一下这种多线程断点续传的方法。

作者信息:
姓名:赵明
email: papaya_zm@sina.com 或 zmpapaya@hotmail.com

对等网络(P2P)被美国《财富》杂志称为改变因特网发展的四大新技术之一,甚至被认为是无线宽带互联网的未来技术。

  P2P技术不仅为个人用户提供了前所未有的自由和便利,同时也试图有效地整合互联网的潜在资源,将基于网页的互联网转变成动态存取、自由交互的海量信息网络。



  P2P技术的发展以及P2P与网格技术的结合,将影响整个计算机网络的概念和人们的信息获取模式,真正实现“网络就是计算机,计算机就是网络”的梦想。


  作为改变现有Internet应用模式的主要技术之一,计算机对等网络(P2P)是目前新一代互联网技术研究的热点之一。


  自1999年以来,P2P的研究得到了国内外学术界和商业组织的广泛关注,同时,由于P2P本质特性不可避免地存在着许多社会、法律和技术上的问题,在学术界和产业界也一直存在着一些怀疑的力量,这在很长一段时期使人们难以对P2P做出一个准确和公平的判断。


  本文较为完整地分析了P2P网络的4种典型结构,并对P2P的主要应用模式、存在的问题以及可能的发展方向进行简要阐述。


1 P2P网络模型
  P2P网络是一种具有较高扩展性的分布式系统结构,其对等概念是指网络中的物理节点在逻辑上具有相同的地位,而并非处理能力的对等。以Napster软件为代表的P2P技术其实质在于将互联网的集中管理模式引向分散管理模式,将内容从中央单一节点引向网络的边缘,从而充分利用互联网中众多终端节点所蕴涵的处理能力和潜在资源。相对于传统的集中式客户/服务器(C/S)模型,P2P弱化了服务器的概念,系统中的各个节点不再区分服务器和客户端的角色关系,每个节点既可请求服务,也可提供服务,节点之间可以直接交换资源和服务而不必通过服务器。


  P2P系统最大的特点就是用户之间直接共享资源,其核心技术就是分布式对象的定位机制,这也是提高网络可扩展性、解决网络带宽被吞噬的关键所在。迄今为止,P2P网络已经历了三代不同网络模型,各种模型各有优缺点,有的还存在着本身难以克服的缺陷,因此在目前P2P技术还远未成熟的阶段,各种网络结构依然能够共存,甚至呈现相互借鉴的形式。

1.1 集中目录式结构
  集中目录式P2P结构是最早出现的P2P应用模式,因为仍然具有中心化的特点也被称为非纯粹的P2P结构。用于共享MP3音乐文件的Napster是其中最典型的代表(见图1),其用户注册与文件检索过程类似于传统的C/S模式,区别在于所有资料并非存储在服务器上,而是存贮在各个节点中。查询节点根据网络流量和延迟等信息选择合适的节点建立直接连接,而不必经过中央服务器进行。这种网络结构非常简单,但是它显示了P2P系统信息量巨大的优势和吸引力,同时也揭示了P2P系统本质上所不可避免的两个问题:法律版权和资源浪费的问题。


1.2 纯P2P网络模型
  纯P2P模式也被称作广播式的P2P模型。它取消了集中的中央服务器,每个用户随机接入网络,并与自己相邻的一组邻居节点通过端到端连接构成一个逻辑覆盖的网络。对等节点之间的内容查询和内容共享都是直接通过相邻节点广播接力传递,同时每个节点还会记录搜索轨迹,以防止搜索环路的产生。


  Gnutella模型是现在应用最广泛的纯P2P非结构化拓扑结构(见图2),它解决了网络结构中心化的问题,扩展性和容错性较好,但是Gnutella网络中的搜索算法以泛洪的方式进行,控制信息的泛滥消耗了大量带宽并很快造成网络拥塞甚至网络的不稳定。同时,局部性能较差的节点可能会导致Gnutella网络被分片,从而导致整个网络的可用性较差,另外这类系统更容易受到垃圾信息,甚至是病毒的恶意攻击。



1.3 混合式网络模型
  Kazaa模型是P2P混合模型的典型代表(见图3),它在纯P2P分布式模型基础上引入了超级节点的概念,综合了集中式P2P快速查找和纯P2P去中心化的优势。Kazaa模型将节点按能力不同(计算能力、内存大小、连接带宽、网络滞留时间等)区分为普通节点和搜索节点两类(也有的进一步分为三类节点,其思想本质相同)。其中搜索节点与其临近的若干普通节点之间构成一个自治的簇,簇内采用基于集中目录式的P2P模式,而整个P2P网络中各个不同的簇之间再通过纯P2P的模式将搜索节点相连起来,甚至也可以在各个搜索节点之间再次选取性能最优的节点,或者另外引入一新的性能最优的节点作为索引节点来保存整个网络中可以利用的搜索节点信息,并且负责维护整个网络的结构。




  由于普通节点的文件搜索先在本地所属的簇内进行,只有查询结果不充分的时候,再通过搜索节点之间进行有限的泛洪。这样就极为有效地消除纯P2P结构中使用泛洪算法带来的网络拥塞、搜索迟缓等不利影响。同时,由于每个簇中的搜索节点监控着所有普通节点的行为,这也能确保一些恶意的攻击行为能在网络局部得到控制,并且超级节点的存在也能在一定程度上提高整个网络的负载平衡。


  总的来说,基于超级节点的混合式P2P网络结构比以往有较大程度的改进。
然而,由于超级节点本身的脆弱性也可能导致其簇内的结点处于孤立状态,因此这种局部索引的方法仍然存在一定的局限性。这导致了结构化的P2P网络模型的出现。

1.4 结构化网络模型
  所谓结构化与非结构化模型的根本区别在于每个节点所维护的邻居是否能够按照某种全局方式组织起来以利于快速查找。结构化P2P模式是一种采用纯分布式的消息传递机制和根据关键字进行查找的定位服务,目前的主流方法是采用分布式哈希表(DHT)技术,这也是目前扩展性最好的P2P路由方式之一。由于DHT各节点并不需要维护整个网络的信息,只在节点中存储其临近的后继节点信息,因此较少的路由信息就可以有效地实现到达目标节点,同时又取消了泛洪算法。该模型有效地减少了节点信息的发送数量,从而增强了P2P网络的扩展性。同时,出于冗余度以及延时的考虑,大部分DHT总是在节点的虚拟标识与关键字最接近的节点上复制备份冗余信息,这样也避免了单一节点失效的问题。


  目前基于DHT的代表性的研究项目主要包括加州大学伯克利分校的CAN项目和Tapestry项目,麻省理工学院的Chord项目、IRIS项目,以及微软研究院的Pastry项目等。这些系统一般都假定节点具有相同的能力,这对于规模较小的系统较为有效。但这种假设并不适合大规模的Internet部署。同时基于DHT的拓扑维护和修复算法也比Gnutella模型和Kazaa模型等无结构的系统要复杂得多,甚至在Chord项目中产生了“绕路”的问题。事实上,目前大量实际应用还大都是基于无结构的拓扑和泛洪广播机制,现在大多采用DHT方式的P2P系统缺乏在Internet中大规模真实部署的实例,成功应用还比较少见。


2 P2P网络应用模式
  Internet最初产生和发展的一个主动力就是资源共享,也正是文件交换的需求直接导致了P2P技术的兴起,这是P2P最初也是最成功的应用之一,也正是针对这类应用的Napster使得人们在客户/服务器模式下开始重新认识P2P思想对人们使用网络习惯的影响。


  随着人们对P2P思想的理解和技术的发展,作为一种软件架构,P2P还可以被开发出种类繁多的应用模式,除了最初的文件交换之外,还出现了一些分布式存储、深度搜索、分布式计算、个人即时通信和协同工作等新颖应用。其中最著名的例子是基于分布式计算的搜索外星文明SETI@home科学实验,每个志愿参加者只需下载并运行类似屏幕保护的方式,就可以贡献自己闲置的计算能力,参与分析Arecibo射电望远镜的无线电磁波数据并回送计算数据,截至2004年12月,已有528万志愿者参加进来,获得了相当于216万年的CPU时间,仅一天的综合计算就相当于67.46Tflops运算。另外,随着SUN公司将其JXTA协议扩展到诸如个人数字助理(PDA)和移动电话等手持终端上,并允许人们屏蔽具体的物理平台进行资料共享和文件交换等,P2P技术在移动通信和智能网领域也开始呈现出较大应用前景。


3 P2P网络存在的问题
  P2P最大的优点在于能够提供可靠的信息查询,但从社会和法律意义来说,绝大多数的P2P服务都将不可避免地遇到知识产权冲突,也可能成为一些非法内容传播的平台。同时由于缺乏中心监管以及自由平等的动态特性,自组织的P2P网络在技术层面也有许多难以解决的问题。


  从某种意义上来说,P2P网络和人际网络具有一定的相似性。一般来说,每个P2P网络都是众多参与者按照共同兴趣组建起来的一个虚拟组织,节点之间存在着一种假定的相互信任关系,但随着P2P网络规模的扩大,这些P2P节点本质所特有的平等自由的动态特性往往与网络服务所需要的信任协作模型之间产生矛盾。激励作用的缺失使节点间更多表现出“贪婪”、“抱怨”和“欺诈”的自私行为,因此P2P中预先假设的信任机制实际上非常脆弱,同时这种信任也难以在节点之间进行推理,导致了全局性信任的缺乏,这直接影响了整个网络的稳定性与可用性。此外,相对于传统客户/服务器模式的服务器可以做主动和被动的防御,由于P2P节点安全防护手段的匮乏以及P2P协议缺乏必要的认证机制和计算机操作系统的安全漏洞,安全问题在P2P网络中更为严重,这将直接影响P2P的大规模商用。另外,P2P网络中的节点本身往往是计算能力相差较大的异构节点,每一个节点都被赋予了相同的职责而没有考虑其计算能力和网络带宽,局部性能较差的点将会导致整体网络性能的恶化,在这种异构节点的环境中难以实现优化的资源管理和负载平衡。同时,由于用户加入离开P2P网络的随意性使得用户获得目标文件具有不确定性,导致许多并非必要的文件下载,而造成大量带宽资源的滥用。特别是大多数P2P用户更喜欢传送音频、视频这些较大的媒体文件,这将使得带宽浪费问题更为突出,尤其在中国大量的用户还是拨号用户,较窄的带宽也成为P2P应用难以逾越的障碍。


4 结论与展望
  P2P技术在最近几年获得了高速的发展,也出现了较多应用,但截至目前,P2P中仍有很多的关键技术问题并没有得到解决,其中最典型的就是带宽吞噬、网络可扩展性差和路由效率低下等问题。这导致P2P至少在目前的技术水平而言只能是一种小范围不可靠的应用或是满足特定任务需求的专门应用。并且,作为一种潜在的商业应用,如何在P2P网络中有效地保护知识产权以及如何设计盈利模式将会面临更为严格的考验。


  未来的网络将呈现大规模分布式、全球性计算和全球性存储的特征,从长远的趋势来看,对于访问和传输服务的需求必将远远大于对于计算功能的需要。尽管P2P技术现在还不成熟,但是迄今为止,至少在理论上P2P仍是最有吸引力的个人通信技术。尤其是P2P与网格技术的结合将是分布式计算技术最有吸引力的发展趋势,虽然现在还没有成熟的方案,但随着分布式系统经典问题的解决以及优化的资源动态分配和资源恢复技术的成熟,P2P与网格技术必将结合起来以影响整个计算机网络的概念和人们的信息获取模式。

下周要进入一个p2p项目组。现在开始熟悉一些这方面的技术,希望能和大家一起学习。