我所了解的Recyclerview绘制流程

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_34501274/article/details/89433711

前言

本文会结合思维导图+源码分析的形式去介绍Recyclerview的绘制流程.。
如果写有什么不对的请大牛指出,十分感谢!

版本

com.android.support:recyclerview-v7:27.1.0

原本选择的是28.0.0版本的,无奈这个版本没有注释,看的脑阔疼,所以最后选择27.1.0

主线介绍

  • 红色:代表主线:recyclerview绘制流程中相比较核心的方法
  • 主线方法标题都会有红色
  • 讲解选择的是LayoutManager的子类:LinearLayoutManager

建议在浏览器中装个可以自动生成目录的插件,这样看起来就不会乱。如图:
在这里插入图片描述

onMeasure源码查看思维导图

本文会按照下图思维导图逐次进行分析Recyclerview的绘制流程,建议看官先过下思维导图,有个印象。

思维导图的方法顺序就是源码中调用的先后顺序
在这里插入图片描述


 @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
        // 第一种情况:LayoutManager对象为空,RecyclerView不能显示任何的数据。
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.isAutoMeasureEnabled()) { //第二种情况:LayoutManager对象为空,RecyclerView不能显示任何的数据。
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
			// 省略N行代码...........
            final boolean measureSpecModeIsExactly =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            if (measureSpecModeIsExactly || mAdapter == null) {
                return;
            }

            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }
          // 省略N行代码...........
            dispatchLayoutStep2();
          // 省略N行代码...........

            // if RecyclerView has non-exact width and height and if there is at least one child
            // which also has non-exact width & height, we have to re-measure.
            // 如果rclerview没有精确的宽度和高度,并且至少有一个子View
            // 子View也没有精确的宽度和高度,我们必须重新测量。
            if (mLayout.shouldMeasureTwice()) {
               // 省略N行代码...........
                dispatchLayoutStep2();
               // 省略N行代码...........
            }
        } else {
       // 第三种情况:LayoutManager没有开启自动测量的情况
           // 省略N行代码...........     
        }
    }

根据源码onMeasure可分为三种情况:

  1. LayoutManager对象为空,RecyclerView不能显示任何的数据。
  2. LayoutManager开启了自动测量时,在这种情况下,有可能会测量两次。
  3. LayoutManager没有开启自动测量的情况,这种情况比较少,因为为了RecyclerView支持warp_content属性,系统提供的LayoutManager都开启自动测量的

这里简单介绍下第三种情况,后面不做详细分析:

//第三种情况的源码:
else {
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
            // 省略N行代码。。。。
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
             // 省略N行代码。。。。
        }

如果mHasFixedSize为true,直接调用LayoutManager的onMeasure方法进行测量,并返回return结束onMeasure

//mHasFixedSize为true 是通过调用此方法进行设置的
   public void setHasFixedSize(boolean hasFixedSize) {
        mHasFixedSize = hasFixedSize;
    }

如果为false,发现最后还是调用LayoutManager的onMeasure方法进行测量。

onMeasure

在这里插入图片描述

defaultOnMeasure(widthSpec, heightSpec);

在这里插入图片描述

// 源码:
  if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }

如果Recyclerview没有调用 setLayoutManager();进行设置 LayoutManager,默认执行 defaultOnMeasure(widthSpec, heightSpec);这个方法,后面直接调用 return结束onMeasure方法的执行。

我们看下 defaultOnMeasure,做了什么:

 /**
     * An implementation of {@link View#onMeasure(int, int)} to fall back to in various scenarios
     * where this RecyclerView is otherwise lacking better information.
     */
    void defaultOnMeasure(int widthSpec, int heightSpec) {
        // calling LayoutManager here is not pretty but that API is already public and it is better
        // than creating another method since this is internal.
        final int width = LayoutManager.chooseSize(widthSpec,
                getPaddingLeft() + getPaddingRight(),
                ViewCompat.getMinimumWidth(this));
        final int height = LayoutManager.chooseSize(heightSpec,
                getPaddingTop() + getPaddingBottom(),
                ViewCompat.getMinimumHeight(this));
                
        // 这里直接进行设置宽高了
        setMeasuredDimension(width, height);
    }

发现内部直接调用 setMeasuredDimension()进行宽高设置了,没用进行子view的测量,所以界面是空白的。

主线:if (mLayout.isAutoMeasureEnabled()) { }

在这里插入图片描述

 public boolean isAutoMeasureEnabled() {
            return mAutoMeasure;
        }
        
 //27.1.0版本中此方法已经废弃
@Deprecated
public void setAutoMeasureEnabled(boolean enabled) {
       mAutoMeasure = enabled;
}

//LayoutManager子类LinearLayoutManager中有此方法:
 @Override
 public boolean isAutoMeasureEnabled() {
        return true;
    }

源码中可以看到, isAutoMeasureEnabled()是直接默认返回 true

提醒:
mLayout.isAutoMeasureEnabled(),因为Recyclerview中mLayout声明的是 LayoutManager mLayout; 
所以点击进去看到的是Recyclerview中的:
      public boolean isAutoMeasureEnabled() {
            return mAutoMeasure;
        }

因为
 //27.1.0版本中此方法已经废弃
@Deprecated
public void setAutoMeasureEnabled(boolean enabled) {
       mAutoMeasure = enabled;
}


所以查看时要去LayoutManager子类LinearLayoutManager中查看:
//LayoutManager子类LinearLayoutManager中有此方法:
 @Override
 public boolean isAutoMeasureEnabled() {
        return true;
    }

if (measureSpecModeIsExactly || mAdapter == null) { return; }

   final boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
           // 如果测量是绝对值,则跳过measure过程直接走layout
            if (measureSpecModeIsExactly || mAdapter == null) {
                return;
            }   

如果宽和高的测量值是绝对值时,直接调用 return 跳过onMeasure 方法。

Measure 的三种模式:

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         * 父控件为子元素指定最大参考尺寸,希望子View的尺寸不要超过这个尺寸
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;
         /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         * 父控件对子控件不加任何束缚,子元素可以得到任意想要的大小
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         * 父控件为子View指定确切大小,希望子View完全按照自己给定尺寸来处理
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

dispatchLayoutStep1();

 //mLayoutStep默认值是 State.STEP_START
    if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }

// RecyclerView.State:
 @LayoutState
 int mLayoutStep = STEP_START;

源码中可以看出: mLayoutStep 的默认值为 State.STEP_START (State为RecyclerView的静态内部类)

 /**
     * The first step of a layout where we;
     * - process adapter updates
     * - decide which animation should run
     * - save information about current views
     * - If necessary, run predictive layout and save its information
     * 布局的第一步;
     * -进程适配器更新
     * -决定应该运行哪个动画
     * -保存有关当前视图的信息
     * -如有必要,运行预测布局并保存其信息
     */
    private void dispatchLayoutStep1() {
    // 省略N多行代码........
     mState.mLayoutStep = State.STEP_LAYOUT;
    }

dispatchLayoutStep1 的作用官方注释已经很清楚了。

执行完 dispatchLayoutStep1 后, mLayoutStep 的值设为 State.STEP_LAYOUT

那这个标识到底是做什么用的呢?

  • State.STEP_START
 mState.mLayoutStep的默认值,还未执行到dispatchLayoutStep1  ,
 执行dispatchLayoutStep1  后 会设置为 State.STEP_LAYOUT
  • State.STEP_LAYOUT
 mState.mLayoutStep = State.STEP_LAYOUT时,表示当前处于layout阶段,
 这个阶段会调用dispatchLayoutStep2方法(调用 onLayoutChildren())。
 调用dispatchLayoutStep2方法之后,此时mState.mLayoutStep变为了State.STEP_ANIMATIONS。
  • State.STEP_ANIMATIONS
 mState.mLayoutStep = State.STEP_LAYOUT,此时处于第三阶段调用动画阶段,会调用dispatchLayoutStep3();
 dispatchLayoutStep3();执行完, mState.mLayoutStep会恢复默认值State.STEP_START

主线:dispatchLayoutStep2()

在这里插入图片描述

 /**
     * The second layout step where we do the actual layout of the views for the final state.
     * This step might be run multiple times if necessary (e.g. measure).
     * 布局的第二步,我们为最终状态实际布局视图。
      *如有需要,此步骤可能会执行多次(例如测量)
     */
    private void dispatchLayoutStep2() {
     // 省略N行代码...........
     // 平时写adaptre时,重写的getItemCount方法这里用到了
      mState.mItemCount = mAdapter.getItemCount();
      // 省略N行代码...........
        // Step 2: Run layout 开始布局
        mState.mInPreLayout = false;
        // 点进去我们会发现LayoutManager实现的是一个空方法
        mLayout.onLayoutChildren(mRecycler, mState);
 
     // 省略N行代码...........
     
      // 更改mLayoutStep 的值
        mState.mLayoutStep = State.STEP_ANIMATIONS;
      // 省略N行代码...........
    }

源码可以看出dispatchLayoutStep2主要做了那些操作:

1. 平时写adaptre时,重写的getItemCount方法这里用到了
2. mLayout.onLayoutChildren:View的绘制交给LayoutManager进行绘制
3. 修改mLayoutStep 的状态 值

4. LayoutManager实现onLayoutChildren是一个空方法,这里需要子类去实现。
这样该怎么布局完全由子类去实现控制,这样就体现了Recycleview的灵活性。

继续往下看:onLayoutChildren

主线:onLayoutChildren()

在这里插入图片描述


    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:
        // 1) by checking children and other variables, find an anchor coordinate and an anchor item position.
        // 2) fill towards start, stacking from bottom
        // 3) fill towards end, stacking from top
        // 4) scroll to fulfill requirements like stack from bottom.create layout state
        //布局算法规则:
       // 1)通过检查子变量和其他变量,找到一个锚坐标和一个锚物品的位置。
       // 2)开始填充,从底部开始堆叠
       // 3)向底填充,从上往下堆叠
       // 4)从底部滚动以满足堆栈等需求。创建布局状态
       
       //省略N行代码.............
        // resolve layout direction 解决布局方向
        resolveShouldLayoutReverse();
        
         final View focused = getFocusedChild();
        if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
                || mPendingSavedState != null) {
            mAnchorInfo.reset();
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // calculate anchor position and coordinate  计算锚点位置和坐标
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        } 
        if (mAnchorInfo.mLayoutFromEnd) {
         //省略N行代码............
          fill(recycler, mLayoutState, state, false);
          //省略N行代码............
          fill(recycler, mLayoutState, state, false);
        //省略N行代码............
           } else { 
          //省略N行代码............
           fill(recycler, mLayoutState, state, false); 
           //省略N行代码............
          fill(recycler, mLayoutState, state, false);
           //省略N行代码............
           
           }
        }
resolveShouldLayoutReverse: 解决布局方向(从方法名来理解是否需要倒着绘制)
updateAnchorInfoForLayout(recycler, state, mAnchorInfo); 计算锚点位置和坐标
//AnchorInfo:重用变量,以保存重新布局的锚信息。为LLM在布局时的参考点提供锚点位置和坐标
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
                || mPendingSavedState != null) {
            mAnchorInfo.reset();
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // calculate anchor position and coordinate  计算锚点位置和坐标
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            //置为true
            mAnchorInfo.mValid = true;
        } 
//AnchorInfo 的构造方法中调用了:
   AnchorInfo() {
            reset();
        }

   void reset() {
       mPosition = NO_POSITION;
       mCoordinate = INVALID_OFFSET;
       mLayoutFromEnd = false;
       mValid = false;
     }    
 // 所以mValid 的默认值得是false
 // 执行完后updateAnchorInfoForLayout后,置为true
updateLayoutStateToFill…()
        同步当前方向上锚点的相关的状态信息。 
主线:调用fill()进行Children的填充

在这里插入图片描述

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
            // 获取View
        View view = layoutState.next(recycler);
       //省略N行代码......
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        // 测量view
        measureChildWithMargins(view, 0, 0);
              //省略N行代码......
 
    }

layoutChunk()主要体现体现的功能:

1. 调用layoutState.next(recycler)获取View
2. addView
3. measureChildWithMargins进行子View测量

主线:layoutState.next(recycler)
 View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            // 根据当前pos 获取View
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }
主线:measureChildWithMargins();
public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
           //省略N行代码......
            if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
            // 进行子view的测量
                child.measure(widthSpec, heightSpec);
            }
        }

到这里dispatchLayoutStep2()的一条主线已经分析完毕

mLayout.shouldMeasureTwice()

在这里插入图片描述
直接看官方注释即可:

 // if RecyclerView has non-exact width and height and if there is at least one child
 // which also has non-exact width & height, we have to re-measure.
 // 如果rclerview没有精确的宽度和高度,并且至少有一个子View
 // 子View也没有精确的宽度和高度,我们必须重新测量。
            if (mLayout.shouldMeasureTwice()) {//会绘制两次
                mLayout.setMeasureSpecs(
                        MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
                mState.mIsMeasuring = true;
                dispatchLayoutStep2();
                // now we can get the width and height from the children.
                //现在我们可以从子元素中得到宽度和高度
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            }

到这里onMeasure的分析就结束了。

注: 以上分析是按照思维导图的顺序依次进行分析的,请结合思维导图进行浏览。

onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
       //省略N行代碼.......
        dispatchLayout();
      //省略N行代碼.......
    }

dispatchLayout();

 void dispatchLayout() {
     //省略N行代碼.......
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
        // 这里可以看到当onMeasure没有执行时,因为mLayoutStep 的默认值是State.STEP_START,
        //这里依然会执行dispatchLayoutStep1(),
        //执行完dispatchLayoutStep1(),会继续执行dispatchLayoutStep2();
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // First 2 steps are done in onMeasure but looks like we have to run again due to
            // changed size.
            // 前两步是在onMeasure中执行,但是之后size又有变化的情况
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
           //始终确保同步(确保模式是精确的) 
            mLayout.setExactMeasureSpecsFrom(this);
        }
        //
        dispatchLayoutStep3();
    }
    

dispatchLayoutStep3

 /**
     * The final step of the layout where we save the information about views for animations,
     * trigger animations and do any necessary cleanup.
     * 布局的最后一步,我们保存关于视图的动画信息,
     * 触发动画并进行必要的清理。
     */
    private void dispatchLayoutStep3() {
       //省略N行代码.......
       // 执行到最后一步 mLayoutStep恢复默认值 
        mState.mLayoutStep = State.STEP_START;
        if (mState.mRunSimpleAnimations) {
            // Step 3: Find out where things are now, and process change animations.
            // traverse list in reverse because we may call animateChange in the loop which may
            // remove the target view holder.
             // 需要动画的情况。找出ViewHolder现在的位置,并且处理改变动画。最后触发动画。
            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
            // 获取Holder
              ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            }
            //省略N行代码.......
            // Step 4: Process view info lists and trigger animations
            //进程视图信息列表和触发动画
            mViewInfoStore.process(mViewInfoProcessCallback);
        }

      
       //省略N行代码.......
       // 完成回调
        mLayout.onLayoutCompleted(mState);
      //省略N行代码.......
    }

onLayoutCompleted();

   @Override
    public void onLayoutCompleted(RecyclerView.State state) {
        super.onLayoutCompleted(state);
        mPendingSavedState = null; // we don't need this anymore
        mPendingScrollPosition = NO_POSITION;
        mPendingScrollPositionOffset = INVALID_OFFSET;
        mAnchorInfo.reset();
    }

最后在回调里重置一些状态信息

onDraw

public void onDraw(Canvas c) {
    super.onDraw(c);
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

到这recyclerview的绘制流程就分析完了。有什么不对的地方麻烦大佬指出,谢谢!

展开阅读全文

我所了解的myeclipse

12-05

myeclipsern安装vipluginrn个人比较喜欢这个插件,因为有了它很强大,和VIM差不多.有兴趣的人可以到网上找找,资料多的是.rnaptana插件不错.可以很好的编辑JS.不过myEclispe7中的那个最好,可以像java一样跳转,不过用了一段时间后感觉有点慢.就没用.7装插件的方式也丰富了很多.rnrnpreferences-General-ContentTypes可以设置文件的字符集.rneditors可以设置不同文件默认的编辑器rnkeys 当然就是设置快捷键了.最上面有个scheme,我用default,eclipse应该还提供了对vim和emacs的支持.不过我不会用.....建议大家都设置自己的快捷键,比如alt+shift+x,j这也太长了..我设置为alt+x.对于经常用到的改改工作效率会高很多的.rn还有就是对于jar包源码的支持.rnpreferences-Java-Installed JREs然后选择你用的jar编辑就行了.常用的就是rt.jar.Java安装目录下有个src.zip指到那个上面就行了.rn也可以安装jad插件.不过就没有注释了.阅读没有注释的代码的时间,比到网上搜索它的源码,再搜索如果将源码正解显示的时间多的多.所以能找到源码时,还是别用jad了.rn有的人喜欢用User Libraries rnpreferences-Java-BuildPath-UserLibraries,其它操作同上.rnjava-CodeStyle下可以设置代码样式...网上很多,不过我没怎么设置,就不说了 .rnjava-Compiler-TaskTags里面有个todo 意思很明显,你要做的任务.有人喜欢在这里设置自己的任务.不过我不用这个.不是因为这个功能不好,是因为同事的代码里很多...很乱.我还是感觉用bookmark比较方便,经常用的人可以考虑在add bookmark上设置快捷键.rnjava-editor-templates有很多模板,也可以设置自己特有的.看看这个有好处的.rn我现在能想起来的也就这么多了.rn以上只是个人理解,有很大可能出现错误...rn对了,对于toolbar上的图标可以自己修改图片.我就把viplugin的换了.其它的懒的找图片的路经,就没换.rn你经常使用下面这么快捷键吗?rnCtrl+Q rnCtrl+L rnCtrl+M rnCtrl+/ rnCtrl+O rnCtrl+T rnCtrl+W rnCtrl+K rnCtrl+E rnCtrl+/rnCtrl+×rnCtrl+Space rnCtrl+Shift+ErnCtrl+J rnCtrl+Shift+JrnCtrl+Shift+FrnCtrl+Shift+XrnCtrl+Shift+YrnCtrl+Shift+FrnCtrl+Shift+PrnAlt+Shift+R rnAlt+Shift+M rnAlt+Shift+C rnAlt+Shift+L rnAlt+Shift+F rnAlt+Shift+I rnAlt+Shift+V rnAlt+Shift+Z rnrnrnrn 论坛

总结我所了解的网络游戏知识3

05-09

第二篇 通信模块的架构及技术难点 《快乐西游》的网络通信模块完全是我手工打造的。 据我所知,windows平台下最高效的网络模型是IOCP即完成端口,linux平台下则是poll. 因为日常的编程是在windows平台上用visual studio完成的,而目标运行平台是linux,所以我提供的网络通信库两个平台都能跑。因 为一开始就定了运行平台是linux,因此没有深入研究iocp,用select简单的实现了一下,程序员自己调试时能正常使用就行了。 当然也有很多借用第三方通信库的,比如ACE等。想想当初自己为了实现windows和linux两个平台都能跑,做了很多#ifdef #else, 如果用ACE就完全不用管这些了,ACE把这些麻烦事都做掉了。ACE还宣称虽然跨平台,但是几乎无损效率。在一般的概念里,要想通用自然就意味着要牺牲某些效率。不过万事无绝对,那些大牛们搞出来的东西必然有它的优势,并且ACE的历史相当的悠久,比stl的历史悠久,比linux的历史也悠久。我现在也在了解ACE的相关内容,或许以后做网络通信模块就用它了。 通信模块说简单也简单,无非就是一个接收,一个发送;说复杂也很复杂,很多细枝末节的问题,处理不好都是致命的。 网络游戏的通信有如下一些特点:长时间连接,上行(客户端到服务器)流量基本稳定,下行流量则严重依赖游戏逻辑状况,响应速度要求很高(ping在150以下才能保证比较好的流畅性)。 一般的web服务,大多一个客户连上来则用一个线程或者进程来专门与之通信,这样简单直接而且有效,客户端断开则线程或者进程结束或者放回池中。因为客 户的连接是相对短期并且客户线程之间基本相互无交互。 基于长时间连接、客户之间交互频繁这两点,网络游戏的处理方法不能这样做,如果为每一个连接开一个线程,当线程数量大到一定程度后线程间的切换与同步带 来的消耗是我们无法接受的。 如果不能为每个客户提供单独的线程来服务,自然就是一个线程服务多个客户了。 最简单的是一个服务所有客户,这样就不存在同步问题了。但是一个线程服务全部客户弊端也是很明显的,客户数量多了以后响应速度会达不到要求,为了保证速度则不得不限制客户数上限,另外单一线程也无法更好利用到系统资源,服务器一般都是多CPU,至少是超线程的,但一线程就完全浪费了这一优势。 既然One vs all 不好,那就只好 1 vs N或者 M vs N,就是说一个线程固定服务多个客户或者是一组线程(或者说一个线程池)服务 一组客户,不管如何,线程同步问题是无法避免了。这就是网络通信模块的一大技术难点-线程同步。 在《快乐西游》中采用的是简单的一个线程服务固定的一组客户的方式。因为是自己手工打造的,要我自己完成线程调度实现线程池我自觉不可能做得很好,所以采用了简单的方式。 接下来的问题是选择阻塞模式还是非阻塞模式。 阻塞模式的好处是简单,函数返回事情肯定做完了(返回错误也可以看作是做完了,因为出错处理是统一的)。问题是一旦堵塞则线程被占,这个线程没办法再拿去做别的事情。这个是难以接受的。为此我在《快乐西游》的网络通信模块中用的是非阻塞模式。 非阻塞模式的好处是不管情况如何函数总能及时返回,线程不会被长期占用,问题是函数返回后,事情可能只做了一部分,对没做掉的部分要做处理,让它下次接着上次没做完的地方继续做,这样实现起来比较复杂,尤其各种出错情况交织其中的时候。非阻塞模式还有一个问题,就是线程空闲的问题,一旦线程没事情可以做的时候我们必须让它停下来不要消耗cpu资源,这个也是比较麻烦的事情。有事情的时候要及时唤醒,无事的时候要及时休息,但是如果要用条件变量等方式唤醒和等待,也不是无代价的,烦啊。我写程序的原则是,尽量往简单里做。所以我用了一个sleep,当线程无事可做的时候sleep 10毫秒,然后检查是否有事可做,没有再sleep 10毫秒。这样做最大的好处是简单,但是存在两个问题,一是长时间无事可做的时候依然会消耗一点cpu资源(因为是即时战斗rpg这种情况会比较少出现),另外一个是突然有事情做的时候最多会引入10毫秒的延迟,平均延迟5毫秒。This is a problem。这是通信模块中的第二个难点,我没有把它解决的很好。 通信模块中的第三个难点是如何减少数据复制。 客户端与服务器之间的数据交换不可能直接用逻辑层的数据结构,因为逻辑运算所用的数据结构往往太大,并且很多运算出来的中间值,网络传递要尽量只传递那些必不可少的数据,这样就必然存在数据的复制,从逻辑数据结构复制到网络通信数据结构,既然是必然要复制,那也没什么问题。 存在问题的地方是这个通信数据结构从逻辑模块到网络通信模块的时候,因为它们在不同的线程中工作,所以这个数据会在线程间传递,必须保证最终发送时依然有效。 这样一来,要么逻辑模块传递给网络通信模块时后者把它复制一份,要么逻辑模块自己new出来,将指针交给网络通信模块,并在最终发送完成以后delete掉。 前一种方法比较符合常规,但是数据要被复制一次;后一种方法则可以省掉复制,但是造成了new和delete的不对称使用,当然我们可以用智能指针等方式解决,不是太大的问题。后一种方式的另外一个问题是如果大同小异的数据要发往多个客户端时,逻辑层使用起来比较麻烦,要重复new很多次并且复制数据很多次,不向前一种那一可以用一个数据实例,只需要修改不同的字段,然后用同样的方法交给网络通信模块即可。 是多一次复制让使用方便呢,还是要求更复杂的使用规则而减少一次memcpy呢,取舍很难。 To be or not to be, it's a problem. 网络通信模块的另外一个技术难点是反外挂。 外挂有很多种,按键精灵类的,修改出入封包的,直接修改属性或者参数的,不一而足。 有些是减轻玩家重复劳动的,可以称为良性外挂,更多的破坏游戏平衡性的,甚至会毁了游戏。几乎每个成功的网络游戏都有很多外挂。最夸张的要数石器时代,RO等游戏的全脱机外挂。完全不用开客户端,只需运行外挂程序,自动做你要做的事情,比人亲自操作还好。 搞笑的是到最后为了查脱机外挂,不得不用gm在游戏里面问玩家问题,不能正常回答的就被认为是脱机外挂,后来外挂制作者根据gm常问的问题提供了自动应答功能。 外挂能做到这种程度,自然是通信协议被完全破解。 因为客户端程序在玩家机器上执行,所以从理论上讲协议完全不被破解是不可能的。网络游戏的破解与反破解,就像博弈,我们是在不可能胜利的一方。 既然是这样我们要做的是什么,能做的是什么呢。 我们要做的是让自己不要输掉,能做的就是加大破解的难度,提供快速应对的方法。 加大破解难度可以从以下几个方面来做: A.给程序加壳。如果不能脱掉壳,则无法准确跟踪程序运行的流程,也无法做静态代码分析。 B.通信协议加密。加密的协议能阻挡简单的通过截获网络封包来破解协议的企图。 C.嵌入动态核心代码。即让一些核心代码在运行时从服务器加载,这样能对抗静态代码分析。 D.同时运行监控程序或者别的方式反调试和反hook。如果不能调试或者向客户端注入代码,很难了解程序运行时到底在做什么。 可能还有一些其他的手段,目的无非是加大破解的难度。但是有句话说得好,道高一尺,魔高一丈。这些手段都是能加大难度,但无法从根本上解决问题。难道我们真的束手无策了么。 换个角度思考问题,会有不同的收获。 为什么会有外挂,为什么会有人来制作外挂? 因为玩家觉得游戏中玩的不爽,需要外挂来帮忙让自己更爽,所以会有外挂。 因为有需求,就有市场,有了市场,自然就有了利益,有了利益,自然就有人来制作外挂。 要想没有外挂,有两种可能,没人使用外挂,或者没人制作。 如果能做到外挂一出,立刻修改协议,让其无法使用,则其无法出售牟利,久而久之制作者会知难而退。如果我们做到破解协议需要两周的时间,而我们每周更换一次协议,那就等于破解者也无能为力。所以说加大破解难度,做到快速应对,很大程度上是对付外挂制作者的。当然还有非技术的方法,起诉外挂制作者,把他们抓起来。 :) 这些都是从环节上解决问题,下面说说从根源上解决问题的办法。 我们让玩家在游戏中没有什么不爽的重复劳动,岂不是从根源上解决问题了么。 理论上是,但是游戏制作无法尽善尽美,另外就是有些玩家的欲望是永无止境的,他总想要破坏规则,达到他想要境界。对这样的玩家,就该像法律处理罪犯一样。 这么说来最好的反外挂方法是两点:一是把游戏做好,让玩家玩得爽;二是做好记录,及时发现破坏规则使用外挂的玩家,立即把它驱逐出游戏(封号,清除角色 ... 关于通信模块的具体包装,可参见另外一篇《关于网络游戏通信模块设计》 rnrn 论坛

总结我所了解的网络游戏知识4

05-09

3.服务器程序框架 因为技术所限,我所做过的游戏的逻辑都是在一个线程内完成的(数据装载或者网络通信当然是多线程的),也就是只有一个循环体。循环体其实也就做几件事,处理输入、响应时间流逝,输出结果。输入可以是来自网络消息,也或者是数据库查询返回,也或者是命令行指令等等;响应时间流逝也就是每个一定的时间,做些跟时间相关的事情;所谓输出运算结果,可以是发送网络消息,也或者是保存数据,也可能只是简单的往log中输出一段文字等等。游戏中的所有事情基本上都可以归入这几件事。 如下的类是我常用的循环体的简短直观描述。 class Circulator public: //初始化 virtual int Initialize(); //释放 virtual void Release(); //主循环体 virtual void Run(); //响应网络封包的输入 virtual OnPacket(PACKET* pPacket); //响应时间的流逝 virtual OnUpdate(unsigned int nElapsed); //发送封包的函数,用来输出结果 virtual SendPacket(PACKET* pPacket); Initialize()和Release()基本上不用说,只在启动和结束的时候执行一次,用来做一些预备和资源释放的工作。 Run()就是循环体的入口,里面基本上是如下一个样子 void Circulator::Run() while(true) //从网络层获取封包 NetworkWrapper::GetPacket(); //处理之 OnPacket(); if( time_elapsed >= HeartBeat_Interval ) //响应时间流逝 OnUpdate(time_elapsed); 接下来说说OnPacket(),这个函数处理所有封包,当然不是做所有的事情,这里仿佛是闸门,闸门后面不可能直接是要灌溉的农田,而是一排整理的渠 道,将出闸的水分流到各个渠道,最终缓缓流入农田。要是闸门后面直接是农田,那么结果就不是灌溉,而是泄洪了。程序也是一样,如果这里直接针对每个网络 消息作处理,这个函数将无比庞大而凌乱,逻辑无法清晰的分开,这对大型的游戏项目来说是灾难性的。 啰嗦了这么多,其实只是想说明OnPacket()只是负责分流工作,将不同的消息交与相关的模块去处理。 其内容大致如下: void Circulator::OnPacket(PACKET* pPacket) switch(pPacket->GetPacketType()) case PACKET_TYPE_Login: LoginModule.OnPacket(pPacket); break; case PACKET_TYPE_Map: MapModule.OnPacket(pPacket); break; case PACKET_TYPE_Mail: MailModule.OnPacket(pPacket); break; ... 这里用的是简单的switch-case,也有人用Command模式,也或者直接把处理函数绑定在Packet中,各有利弊,不一一详述。 既然上面提到了LoginModule和MapModule以及MailModule等,再来说说另外一个比较重要的东西:逻辑模块。我喜欢把一些相关 的逻辑处理组织在一起做成一个个模块,既高效的完成功能,又尽量相互独立,降低耦合。下面给出一个模块的基类的简单描述来勾勒其工作的方式。 class ModuleBase public: virtual void OnPacket(PACKET* pPacket); virtual void OnUpdate(unsigned int nElapsed); void RaiseEvent(EVENT* pEvent); void OnEvent(EVENT* pEvent); 前面两个函数不用细说,一看就知道,就是前面提到的闸门后面的渠道,用来处理分流进来的各种输入。后面一个函数RaiseEvent(),其作用是什么 呢。故名思义,发起事件。我们知道,如果每个模块都能这么简单的包装完全相互独立,那我们干嘛还要用单线程,给每个模块一个线程不是能很大程度上提高处 理能力么。事实是各模块之间不但要公用玩家信息、地图信息、公会信息等等数据,还要相互之间关联,不可能完全独立。但是我也不希望他们之间相互直接引 用,那样会造成头文件包含灾难和逻辑混乱。为此做了EVENT,来提供一种方式让他们可以相互沟通,但不至于耦合。举个例子,玩家登录我们假定是 LoginModule来处理的,但是公会模块也关心玩家上线的事情,可能要为这个玩家准备相关的数据。我们可以直接在LoginModule的 OnLoginOK()里面直接调用GuildModule.OnPlayerLogin(),那如果邮件模块也关心这个事情,那么我们还得加入 MailModule.OnPlayerLogin(),这就是我前面提到的强耦合的方式。用EVENT的方式是在OnLoginOK() 中,RaiseEvent(EVENT_Login),这个函数做什么呢,是把这个事件交给Circulator,让它来处理,Circulator的 处理也很简单,就是根据自己记录的哪些模块关心此事件,就调用它的OnEvent方法来让其有机会响应。这里用到了一个简单的解耦合的方法,也是编程中 常用的方法,解除了各个Module之间的耦合性,各模块只和Circulator之间有耦合,而Circulator是唯一的循环体,和它的耦合是不 可避免的。当然我们可以用同样的注册Packet类型和逻辑模块的方式来降低Circulator和各具体模块之间的耦合,让Circulator只和 ModuleBase之间发生耦合(当然也有个代价问题值得考虑,因为网络封包相当的多,即OnPacket函数的调用相当的频繁,其分流效率一定要 高。很明显switch-case不是一个最高效的方式,但是用来说明服务器程序组织架构很适合:)。 说完了OnPacket,其实OnUpdate也不需要再说了,因为他们的分流方式是一样的,只不过最终的具体模块中他们做的事情不一样而以。如果用抽象的眼光来看,时间流逝也可以看作是一种输入,它和网络封包这类的输入并没有本质的不同,各模块的功能也都是接受一定的输入,计算之,产生输出。其实输 出也可以把他们统一管理起来。如果输出被其他的方式接管,那么模块的功能就可以归纳为一个词--计算服务。各模块所做的事情就像是一架香肠机,在入口丢进去一头猪,在出口整齐的摆出香肠。:)和香肠机有所区别的是,模块提供计算服务的时候需要有些环境数据,比如玩家信息、地图信息等。也可以说功能模块需要数据服务。那正好,我们做一些模块来完成数据服务,不同的数据服务有不同的提供者。比如玩家数据我们可以用一个PlayerMgr来管理并且提供个各功能模块,地图数据我们可以用MapMgr来完成。 对于PlayerMgr可以是一个player list,也可以是一个map,当然只要能完成所需要的功能,用什么方法是不要紧的,面向对象的方法就是封装实现细节。我前面有篇文章提到一个小技巧来高效的组织网游服务器中的数据,文章题目是《小技巧,大作用》。 提到数据服务,还有一种数据服务也要说说,那就是数据存储服务。 rnrn 论坛

ssh中我所不能了解的错误。。。

09-20

ssh框架,写了添加功能和列表功能,都正常运行。又写了按照id查询,报出如下错误:rnrnorg.springframework.orm.hibernate3.HibernateQueryException: unexpected AST node: + near line 1, column 32 [from com.pojo.Admin where id = +id]; nested exception is org.hibernate.hql.ast.QuerySyntaxException: unexpected AST node: + near line 1, column 32 [from com.pojo.Admin where id = +id]rn org.springframework.orm.hibernate3.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:660)rn org.springframework.orm.hibernate3.HibernateAccessor.convertHibernateAccessException(HibernateAccessor.java:412)rn org.springframework.orm.hibernate3.HibernateTemplate.doExecute(HibernateTemplate.java:411)rn org.springframework.orm.hibernate3.HibernateTemplate.executeWithNativeSession(HibernateTemplate.java:374)rn org.springframework.orm.hibernate3.HibernateTemplate.find(HibernateTemplate.java:912)rn org.springframework.orm.hibernate3.HibernateTemplate.find(HibernateTemplate.java:904)rn com.dao.impl.adminDaoImpl.queryAdminId(adminDaoImpl.java:26)rn com.service.impl.adminServiceImpl.ListAdminId(adminServiceImpl.java:28)rn com.action.adminAction.ListAdminId(adminAction.java:27)rn sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)rn sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)rn sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)rn java.lang.reflect.Method.invoke(Method.java:597)rn com.opensymphony.xwork2.DefaultActionInvocation.invokeAction(DefaultActionInvocation.java:453)rn com.opensymphony.xwork2.DefaultActionInvocation.invokeActionOnly(DefaultActionInvocation.java:292)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:255)rn org.apache.struts2.interceptor.debugging.DebuggingInterceptor.intercept(DebuggingInterceptor.java:256)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.DefaultWorkflowInterceptor.doIntercept(DefaultWorkflowInterceptor.java:176)rn com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.validator.ValidationInterceptor.doIntercept(ValidationInterceptor.java:265)rn org.apache.struts2.interceptor.validation.AnnotationValidationInterceptor.doIntercept(AnnotationValidationInterceptor.java:68)rn com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor.intercept(ConversionErrorInterceptor.java:138)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:211)rn com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:211)rn com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.StaticParametersInterceptor.intercept(StaticParametersInterceptor.java:190)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn org.apache.struts2.interceptor.MultiselectInterceptor.intercept(MultiselectInterceptor.java:75)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn org.apache.struts2.interceptor.CheckboxInterceptor.intercept(CheckboxInterceptor.java:90)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn org.apache.struts2.interceptor.FileUploadInterceptor.intercept(FileUploadInterceptor.java:243)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor.intercept(ModelDrivenInterceptor.java:100)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor.intercept(ScopedModelDrivenInterceptor.java:141)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.ChainingInterceptor.intercept(ChainingInterceptor.java:145)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.PrepareInterceptor.doIntercept(PrepareInterceptor.java:171)rn com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.I18nInterceptor.intercept(I18nInterceptor.java:176)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn org.apache.struts2.interceptor.ServletConfigInterceptor.intercept(ServletConfigInterceptor.java:164)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.AliasInterceptor.intercept(AliasInterceptor.java:192)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor.intercept(ExceptionMappingInterceptor.java:187)rn com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)rn org.apache.struts2.impl.StrutsActionProxy.execute(StrutsActionProxy.java:54)rn org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:511)rn org.apache.struts2.dispatcher.ng.ExecuteOperations.executeAction(ExecuteOperations.java:77)rn org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:91)rnrnrn 论坛

总结我所了解的网络游戏知识2

05-09

第一篇 Mmorpg服务器架构 《快乐西游》http://joyxy.the9.com 的服务器框架 Client <--> GameServer <--> GroupServer <--> Database (可惜这里不能贴图,只能简单的文字说明,但愿不要引起误解) 客户经过验证直接连线到游戏服务器(一个游戏服务器称为一线),5-10个GameServer形成一组,受一个GroupServer管理,加上一个 帐号服务器,形成一个区。一个完整的区包含10(GameServer)+1(GroupServer)+1(AccountServer) +1(DB)共13台机器,能负载5000-10000人同时在线。 这种框架的典型特点是区与区之间帐号不通用,线与线之间世界是独立的世界,即你在一线的武器铺,我在二线的武器铺我们是看不到对方的,我们处于一个完全 相同的世界副本中。 九城几年前代理的游戏《奇迹》也是这种架构,只是它更多一个层次,叫做服务器组。 从程序的角度来看就是 一个游戏世界由一个进程来维持。 《魔兽世界》http://wowchina.com 的服务器框架 Client <-->GateServer <--> GameServer <--> Database 客户经过验证之后连线到GateServer,GateServer再连接GameServer,一个游戏世界由多个GameServer支撑,可能这个GameServer管几个地图,那个GameServer管另外几个地图, 另外一个GameServer则负责副本地图等等,这就是为什么有时候铁炉堡的人集体掉线但是达纳苏斯的都没事,或者MC的集体掉线而在燃烧平原做任务的一点影响都没有,那都是因为负责那块地图的GameServer崩溃了。当然崩溃有时候也会引起错误漫延,导致不得不全服(所有GameServer)重启。 从程序的角度来看就是 一个游戏世界由多个进程来维持。 (注:本人没有真正接触到wow的服务器,仅凭借自己看到的现象来推测服务器的框架,快乐西游服务器是我负责设计的,所以能说的很明确。) 一个世界由单一进程来维持,做起来相对简单,玩家切换世界跟下线之后重新登录基本没有区别,无需处理很多边界问题。问题就在于一个世界容纳的人数有限, 这种即时战斗的rpg,一个进程能支持到1000人算很好了(普通2G的志强CPU的服务器)。 多进程支撑一个世界的好处是游戏世界可以做的很大,容纳很多人一起游戏,提供的交互性更好,但是游戏程序的复杂性也会大很多。玩家角色在两个服务器负责 的边界区域来回游走或者交易时,同步问题比较复杂。曾有过游戏(忘记了是什么游戏了)就在这个同步上出了问题导致大量物品复制。当然也不只是地图的管理 在不同的进程,有的游戏还把不同的逻辑运算分散到不同的进程,比如游走处理放在一个进程,任务处理放在另外一个进程等,这样同样是存在公用数据的同步问 题。Wow可能就用了这种,因为在wow中曾碰到过一个任务交了以后领到任务物品,服务器重启后再上来任务可以再交一次领到同样的任务物品,当然也可能 只是任务和数据存储方面的bug。 因为多进程维持一个世界能给玩家带来更好的游戏体验,现在是越来越流行,新的游戏几乎都是这种方式。 rnrn 论坛

总结我所了解的网络游戏知识5

05-14

4。网络游戏的一大乐趣是可以积累。所谓积累,最终反映在程序上就是一个可以长久保留的数据,存在 文件中或者数据库中。为此,数据的存储也是网络游戏一个很重要的部分。 国内最早的网络游戏《万王之王》,其核心来自于以前的文字MUD,由MUDOS和脚本组成。其玩家数据的存储是用文件形式的,并以首字母为索引分目录。 现今的网络游戏基本上都使用了数据库。因为数据库管理大量规则数据的优势明显,结构化查询语言(SQL)提供丰富的查询功能,完善的数据库系统产品提供简便而完备的维护功能。如果直接使用文件,这些功能都要自己实现,有些类似于重复发明轮子。 《快乐西游》的数据库我选择的是MySQL,主要是基于几点:免费、效率高、功能简单够用。严格来讲MySQL不能算是数据库,因为其没有提供事务功 能,只能算是一个提供SQL的文件系统。(新版本的MySQL好像已经实现了事务功能)功能简单可能也正是它高效率的原因。因为它提供了我所需要的所有功能,够用就好。 在大学毕业之前曾帮一家公司做过MIS(管理信息系统),用过Oraclehe和MS SQL Server。也正是那个时候学到的SQL,学到的数据库的一些基本知识。当时已经有很好的图形工具来建立数据库和表结构,十分方便直观,但是指导我的一个长者坚持让我用脚本来完成这些工作。我很不解,放着这么好的可视化工具不用,非要自己去抠一些规则细节来写脚本是何苦?得到的回答是:你以后就明白其中的好处了。是的,很快我就明白了数据库脚本的好处。 移植、迁移、重建时脚本是如此的容易,而图形化的工具则要重复劳动并且无法确保自己不遗漏东西。当然,现在的图形化工具都提供导出脚本的功能了,但是我依然改不了自己写脚本的习惯。 旧事不提了,言归正传。网络游戏的数据相对于大型企业的数据来讲简直就是小儿科,表结构简单,需要的查询功能也少,但是有一个重要的要求就是查询频度高并且要求查询效率高,这些特点也是我选择MySQL的重要原因。 《快乐西游》的数据库接口可以分成两个部分,一个是数据库访问接口,一个是程序的查询管理。前者我采用的MySQL的C语言API实现,封装为一个类, 其核心就是一个ExecuteSQL函数,其实也是对MySQL提供的C语言接口的一些简单的封装,没有太多需要细说的地方。查询管理我也封装了一个名为QueryMgr的类。 因为查询数据的函数调用不可能即刻返回,主线程又不可能在这里等着(否则什么事都不要做了:),所以需要有个查询管理器来管理这些查询及其返回的结果。 查询管理器有个有个后台线程来等待查询的结果,并将结果还给发起查询请求的主线程,示例代码如下: class Query //查询管理器与主线程交换信息的数据结构或者说通信协议 public: int nQueryID;//查询ID,用来标志不同的查询 void *pData;//查询附带的数据 void *pTage;//查询附加标志,查询发起者在查询返回时需要的一些发起时的数据区分。 int nResult;//查询结果 class QueryMgr public: int Initialize();//初始化一些东西,并启动后台线程,等待在查询队列上 void AddQuery(Query* pQuery);//向查询队列中加入一个查询 Query* GetQuery();//从查询队列中取出一个等待完成的查询 Query* GetQueryFinished();//从已经完成的查询队列中取出查询,供主线程调用 void FinishQuery(Query* pQuery);//查询完成,加入到完成队列 void ThreadProc()//线程函数 while ( bWorking ) Query* pQuery = GetQuery(); if( pQuery != NULL) pQuery->nResult = ProcessQuery(pQuery); FinishQuery(pQuery); int ProcessQuery(Query* pQuery)//处理查询,也就是执行相关的查询语句并得到结果 switch(pQuery->nQueryID) case .... //执行查询并记录结果 break; protected: ThreadSafeQueue queue_; ThreadSafeQueue queueFinished_; 主线程的OnUpdate()中加入如下一段代码 Query* pQueryFinished(NULL); while( pQueryFinished= QueryMgr->GetQueryFinished() ) != NULL ) switch ( pQueryFinished->nQueryID) ... 大致情况如上。这样做的好处是简单明了,想做什么查询只需要调用QueryMgr->AddQuery()即可,然后定时去调用QueryMgr->GetQueryFinished()得到结果处理之。如果需要,可以把用法改的更漂亮一点,把这些switch-case都用回调函数的方式替代。 为Query类添加一个Exec(CallbackFunction func)函数,一个SetQueryMgr(QueryMgr*)函数(如果只 使用一个QueryMgr则可以把QueryMgr做成单体,这样SetQueryMgr()函数都不需要了)。Exec()函数调用QueryMgr的AddQuery()把自己和自己的回调函数加入到对列中(QueryMgr也需要做相应的修改支持回调函数功能),当完成查询之后调用该回调函数执行后续的功能。 《快乐西游》中的GameServer直接使用我上面提供的接口访问数据库,因为有多个GameServer可能同时访问同一个数据库,这中间就需要有 个同步问题:即数据更新后可能在没有被写入数据库之前,数据库又被读取了。同步的问题在测试之初并没有解决,所以当时曾出现过因此引起物品复制问题(同 步实现之后就不再有此引起的复制问题,更多的物品复制问题源自于程序逻辑漏洞而非数据库接口问题)。 为了解决数据库同步问题,曾想到过一个方案:在GameServer和Database之间添加一个中间层。所有的数据访问由这个中间层来执行,GameServer只和这个中间层来交换数据,就算数据的修改没有及时提交给数据库,但是中间层内的数据已经被更新。其实这种方式也是有同步问题的,只是不容易显现而已。其实此方案最大的好处是能统一组织数据,提供高效率的数据访问,减少数据库读写次数。 rnrn 论坛

没有更多推荐了,返回首页