终极网络服务端编程--第一章【网络模块】

终极网络服务端编程--第一章【网络模块】

 

 

第一章 网络模块

1.1网络编程概述/基本socket api

 

很多网络游戏通讯都是客户端/服务器结构。 简写Client/Server 或是c/s 。

c/s架构允许N个Client连接1个或多个Server来进行通讯。

做网页的朋友就会提出 还有b/s架构。其实b/s 是在以tcp为通讯基础的数据封装协议(web协议)。底层还是用的tcp。

 

通常大部分Server都是基于tcp协议的(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议)

比如网络游戏,web 上网看视频,语音文字聊天 99%都是tcp协议通讯的,而且是c/s架构的。

 

基于udp协议的Server比较少,而且udp协议(User Datagram Protocol 提供面向事务的简单不可靠信息传送服务) 由于传输不可靠,所以在使用上需要再封装上一些技术来保证消息传输的可靠性,编码难度上升不少

udp通常用于p2p架构(比如bt下载),内网传输视频广播会议等。

 

 

本书大部分都是着重讲解tcp协议的通讯为主。

 

 

1.2 网络通信协议

开始需要先了解网络通信协议,都是一些相关知识,还是有必要了解一下的。

1.2.1网络协议

要交换信息,必定有相关的规则,这些规则的集合就称为协议(Protocol)

那么常用的网络协议TCP/IP就是一个协议的集合。

1.2.2 ISO/OSI七层参考模型

OSI七层参考模型是ISO国际标准化组织制定的一个逻辑上的定义,一个规范,它把网络从逻辑上分为了7层。

 

 

OSI的7层从上到下分别是

7应用层

6表示层

5会话层

4传输层

3网络层

2数据链路层

1物理层

其中高层,即7、6、5、4层定义了应用程序的功能,下面3层,即3、2、1层主要面向通过网络的端到端的数据流

 

OSI七层参考模型仅供参考,虽然它设计的很好,可惜出炉的太晚,我们实际网络通信中用的还是TCP/IP协议。

1.2.3 TCP/IP协议

网络通讯协议,是Internet最基本的协议、Internet国际互联网络的基础,由网络层的IP协议和传输层的TCP协议组成。

TCP/IP定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。协议采用了4层的层级结构:网络访问层、互联网层、传输层和应用层

 

 

 

网络访问层(NetworkAccess Layer)在TCP/IP参考模型中并没有详细描述,只是指出主机必须使用某种协议与网络相连。

互联网层(InternetLayer)是整个体系结构的关键部分,其功能是使主机可以把分组发往任何网络,并使分组独立地传向目标。这些分组可能经由不同的网络,到达的顺序和发送的顺序也可能不同。高层如果需要顺序收发,那么就必须自行处理对分组的排序。互联网层使用因特网协议(IP,InternetProtocol)。TCP/IP参考模型的互联网层和OSI参考模型的网络层在功能上非常相似。

传输层(TramsportLayer)使源端和目的端机器上的对等实体可以进行会话。在这一层定义了两个端到端的协议:传输控制协议(TCP,TransmissionControl Protocol)和用户数据报协议(UDP,UserDatagram Protocol)。TCP是面向连接的协议,它提供可靠的报文传输和对上层应用的连接服务。为此,除了基本的数据传输外,它还有可靠性保证、流量控制、多路复用、优先权和安全性控制等功能。UDP是面向无连接的不可靠传输的协议,主要用于不需要TCP的排序和流量控制等功能的应用程序。

应用层(ApplicationLayer)包含所有的高层协议,包括:文件传输协议(FTP,FileTransfer Protocol)、电子邮件传输协议(SMTP,SimpleMail Transfer Protocol)、域名服务(DNS,DomainName Service)和超文本传送协议(HTTP,HyperTextTransfer Protocol)等。 FTP提供有效地将文件从一台机器上移到另一台机器上的方法;SMTP用于电子邮件的收发;DNS用于把主机名映射到网络地址;HTTP用于在WWW上获取主页。

 

 

 

TCP/IP结构对应OSI

TCP/IP

OSI

应用层

应用层

表示层

会话层

主机到主机层(TCP)(又称传输层)

传输层

网络层(IP)(又称互联层)

网络层

网络接口层(又称链路层)

数据链路层

物理层

 

 

 

 

 

1.2.4 TCP/IP协议族

 

 

 

 

虽然是TCP/IP协议,但TCP协议和IP协议只是众多协议中的两个,因为他们比较重要,所以就用TCP/IP来称呼整个协议族。除了TCP/IP 其实还有其他协议的,比如UDP,ICMP,IGMP,ARP。

 

 

我们网络编程主要就是使用TCP/UDP这两种协议。而本书主要讲解的就是TCP。

1.2.5 总结

 

 

 

总结

OSI中的层

功能

TCP/IP协议族

应用层

文件传输,电子邮件,文件服务,虚拟终端

TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet等等

表示层

数据格式化,代码转换,数据加密

没有协议

会话层

解除或建立与别的接点的联系

没有协议

传输层

提供端对端的接口

TCP,UDP

网络层

数据包选择路由

IP,ICMP,OSPF,EIGRP,IGMP

数据链路层

传输有地址的帧以及错误检测功能

SLIP,CSLIP,PPP,MTU

物理层

以二进制数据形式在物理媒体上传输数据

ISO2110,IEEE802,IEEE802.2

 

网络编程主要是在传输层,使用TCP或UDP.网络层什么的了解一下就行了,除非你需要更底层的编程,比如发送自定义IP头数据包(无连接无端口,网络层级别)、发送ICMP包(ping包)、网卡sniffer抓数据包(winpcap)。想要深入学习了解更多可以去看《Windows网络编程技术》《TCP/IP详解》

 

 

1.3 Socket编程原理

TCP/IP传输层采用的是Socket接口来实现,避免了开发人员直接面对复杂的多层网络协议。

1.3.1 套接字(Socket)

Socket通常中文称作套接字。用于描述IP地址和端口,目的就是为了便于不同机器编程通信。 只要我们知道对方的IP和端口,我们就可以用Socket和对方相互通信。

 

 

Socket主要分为两种,一种是流式Socket(即是TCP协议用的socket),一种是数据报式Socket(即是UDP协议用的Socket)。本书主要讲流式Socket。

Socket是网络 i/o的基础,linux和windows都提供了socket接口。

Linux提供的是标准Socket,又称Berkeley Socket。

Windows提供了winsock,是微软在标准socket基础上增加了一些windows的扩展,主要用于windows平台下开发。

 

1.3.2 Socket通信流程

 

socket是"打开—读/写—关闭"模式的实现,以使用TCP协议通讯的socket为例,其交互流程大概是这样子的

 

 

 

 

服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket

服务器为socket绑定ip地址和端口号

服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开

客户端创建socket

客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket

服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求

客户端连接成功,向服务器发送连接状态信息

服务器accept方法返回,连接成功

客户端向socket写入信息

服务器读取信息

客户端关闭

服务器端关闭

 

 

先来看一个最简单winsock c/s例子。下面是Server

 

Server.cpp

 

#include<stdio.h>

#include<winsock.h>

#pragma comment(lib,"Ws2_32")

#define MAXDATASIZE 100                          /* 每次可以接收的最大字节 */

 

int _tmain(int argc, _TCHAR* argv[])

{

       int sockfd, new_fd;                       /*    定义套接字        */

       struct sockaddr_in my_addr;         /*    本地地址信息    */

       struct sockaddr_in their_addr;/*    连接者地址信息       */

       int sin_size;

 

       WSADATA ws;

       WSAStartup(MAKEWORD(2,2),&ws);     //初始化WindowsSocket Dll

 

       //建立socket

       sockfd = socket(AF_INET, SOCK_STREAM,0);

 

       //bind本机的端口

       my_addr.sin_family= AF_INET;                /* 协议类型是INET */

       my_addr.sin_port = htons(800);            /* 绑定端口     */

       my_addr.sin_addr.s_addr = INADDR_ANY;   /* 本机IP                  */

 

       bind(sockfd, (struct sockaddr*)&my_addr,sizeof(struct sockaddr)); 

 

       //listen,监听端口

       listen(sockfd, 5);

       printf("listen......\n"); 

       //等待客户端连接

       sin_size = sizeof(struct sockaddr_in);

       new_fd = accept(sockfd, (struct sockaddr *)&their_addr,&sin_size);

 

 

       //接收数据并打印出来

       char buf[MAXDATASIZE];

       int numbytes=recv(new_fd, buf, MAXDATASIZE,0);

       buf[numbytes] ='\0';

       printf("Received: [%s]",buf);

 

       //echo

       send(new_fd,  buf ,  strlen(buf) , 0);

       printf("send ok!\n");

 

 

       // 关闭套接字

       closesocket(sockfd);

       closesocket(new_fd);

 

       return 0;

}

 

 

下面是Client.

 

Client.cpp

 

#include<stdio.h>

#include<stdio.h>

#include<winsock.h>

#pragma comment(lib,"Ws2_32")

#define MAXDATASIZE 100                          /* 每次可以接收的最大字节 */

 

#define SERVER_IP "127.0.0.1"

#define SERVER_PORT 800

 

int _tmain(int argc, _TCHAR* argv[])

{

        

 

       WSADATA ws;

       WSAStartup(MAKEWORD(2,2),&ws);     //初始化WindowsSocket Dll

 

       int sockfd = socket(AF_INET, SOCK_STREAM, 0);

             

       sockaddr_in their_addr;    /* 对方的地址端口信息 */

       //连接服务器

       their_addr.sin_family= AF_INET; /* 协议类型是INET       */

       their_addr.sin_port= htons(SERVER_PORT);/*连接对方800端口 */

       their_addr.sin_addr.s_addr = inet_addr(SERVER_IP);/*连接对方的IP  */

       connect(sockfd, (struct sockaddr*)&their_addr,sizeof(struct sockaddr));

      

       //发送字符串给服务器

       char buf[MAXDATASIZE]="hello socket!";

       send(sockfd, buf , strlen(buf), 0);

       printf("send ok!\n");

 

 

       //接收数据并打印出来

       char buf2[MAXDATASIZE];

       int numbytes=recv(sockfd, buf2, MAXDATASIZE,0);

       buf2[numbytes] ='\0';

       printf("Received: [%s]",buf2);

 

       //关闭套接字

       closesocket(sockfd);

       return 0;

}

 

 

 

 

 

先运行Server,若运行成功,则会监听800端口,并等待客户连接。然后运行控制台,运行 Client127.0.0.1  ,若成功,则会打印出hello的字符串,然后server/client都网络断开。

 

1.3.3 Socket基本api介绍

WSAStartup   初始化Windows Socket Dll

 

例:     

WSADATA ws;

       WSAStartup(MAKEWORD(2,2),&ws);    

 

 

 

socket  创建socket句柄

SOCK_STREAM参数表示创建流式socket即tcp socket ,SOCK_DGRAM参数表示创建数据报socket即udp socket 。

例:

sockfd = socket(AF_INET, SOCK_STREAM, 0);

 

 

 

connect  连接指定的ip和端口

 

例:

       their_addr.sin_family = AF_INET;                             /* 协议类型是INET       */

       their_addr.sin_port = htons(800);                               /* 连接对方800端口     */

       their_addr.sin_addr.s_addr = inet_addr(argv[1]);      /* 连接对方的IP     */

       connect(sockfd, (struct sockaddr *)&their_addr,sizeof(struct sockaddr));

 

 

bind 将一本地地址与一套接口捆绑。本函数适用于未连接的数据报或流类套接口,在connect()listen()调用前使用 。

简而言之:绑定端口,即绑定指定ip端口在的网卡上。如果bind失败可能是端口已经被占用了。

例:

       my_addr.sin_family = AF_INET;                 /* 协议类型是INET       */

       my_addr.sin_port = htons(800);            /* 绑定 端口     */

       my_addr.sin_addr.s_addr = INADDR_ANY;    /* 本机IP                 */

      

       bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)); 

 

 

listen 创建一个套接口并监听申请的连接.简而言之: 就是监听端口。Listen调用成功后,端口就可以被其他socket connect了。如果listen失败可能是端口已经被占用了。

例:

listen(sockfd, 5);

 

 

accept 在一个套接口接受一个连接, 后2个参数用来接收为通讯层所知的连接实体的地址(即对方的地址),参数可以为空,表示不关心(需要)对方ip端口相关信息。

例:

new_fd = accept(sockfd, NULL,NULL);

 

 

send  用来将数据由指定的 socket传给对方主机。使用 send 时套接字必须已经连接

参数说明

第一个参数指定发送端套接字描述符;

第二个参数指明一个存放应用程式要发送数据的缓冲区

第三个参数指明实际要发送的数据的字节数;

第四个参数一般置0。

例:

send(new_fd, "hello\n", 6, 0);

 

 

recv  用于已连接的流式套接口进行数据的接收。参数说明sockfd参数是已建立连接的套接字,将在这个套接字上发送数据。第二个参数 buf,则是字符缓冲区,区内包含即将发送的数据。第三个参数 len,指定即将发送的缓冲区内的字符数。最后,flags可为0

例:

       numbytes=recv(sockfd, buf, len, 0);

 

 

 

closesocket  关闭指定socket网络连接。

例:

closesocket(sockfd);

 

 

1.3.4 Socket api总结

那么作为Server的api调用过程类似如下

WSAStartup(初始化环境);

      

 Socket(申请socket句柄);

    

 Bind(ip端口绑定);

    

 Listen(开始监听);

      

 while(true)

 {

      Accept(接受连接)

      Recv(接收)  Send(发送)

      Closesocket(关闭对方连接)

 }

 

 

Client api调用过程更简单,就不多解释了。

 

那么有的同学会问了,Server使用循环,在while里 accept一个连接,一次只能给一个连接通讯,直到对方断开,才能接受新连接。这种代码模型称之为串行。当网络需求量不大的时候,这种模型其实够用了,但当大量连接同时通讯,它的弊端就出现了,无法及时响应了,有没有更好的办法来和大量的客户socket连接同时通讯呢?答案是有很多种!我们将在下一节里讲述。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

1.4 并发 一线程一客户模型

接着上一节,我们讲到了socket在while循环里一次只能和一个客户端通讯,那么如何在一个线程里和大量的socket客户端通讯?答案就是并发!

Socket并发,最简单的方法就是开线程。下面是一个代码例子

 

 

onethread one client io模型

 

 while(1)

{

       sockaddr_in adr;

       int nAddrLen = sizeof(adr);

       SOCKET c= accept(s, (struct sockaddr*)& adr, &nAddrLen);

if (c!=INVALID_SOCKET )

{

     //为一个socket client 开启单独的接收线程

               HANDLE h=::CreateThread(0,0, client_thread ,(LPVOID)c,0,NULL);

               ::CloseHandle(h);

       }

    …..

}

 

//客户线程

DWORD WINAPIclient_thread(LPVOID lp)

{

       SOCKET c=(SOCKET)lp;

   

 

    可以在这里和client收发通讯。

}

 

 

下面贴出完整代码,ThreadEchoServer

 

ThreadEchoServer.cpp

 

#include<stdio.h>

#include<winsock.h>

#pragma comment(lib,"Ws2_32")

#define MAXDATASIZE 100                          /* 每次可以接收的最大字节 */

 

//客户线程

DWORD WINAPI client_thread(LPVOID lp)

{

       SOCKET c=(SOCKET)lp;

 

 

 

       //接收数据并打印出来

       char buf[MAXDATASIZE];

       int numbytes=recv(c, buf, MAXDATASIZE,0);

       buf[numbytes] ='\0';

       printf("Received: [%s] \n",buf);

 

       //echo

       send(c,  buf ,  strlen(buf) , 0);

       printf("send ok!\n");

 

 

       // 关闭套接字

       closesocket(c);

 

       return 0;

}

 

int _tmain(int argc, _TCHAR* argv[])

{

 

       WSADATA ws;

       WSAStartup(MAKEWORD(2,2),&ws);     //初始化WindowsSocket Dll

 

       //建立socket

       int sockfd = socket(AF_INET, SOCK_STREAM, 0);

 

       sockaddr_in my_addr;             /*    本地地址信息    */

       //bind本机的端口

       my_addr.sin_family= AF_INET;                /* 协议类型是INET */

       my_addr.sin_port = htons(800);            /* 绑定端口     */

       my_addr.sin_addr.s_addr = INADDR_ANY;   /* 本机IP                  */

 

       bind(sockfd, (struct sockaddr*)&my_addr,sizeof(struct sockaddr)); 

 

       //listen,监听端口

       listen(sockfd, 5);

       printf("listen......\n"); 

       //等待客户端连接

        

       while(1)

       {

              int sin_size;

              sockaddr_intheir_addr;    /*    连接者地址信息       */

              sin_size= sizeof(structsockaddr_in);

              SOCKET c = accept(sockfd, (struct sockaddr *)& their_addr,& sin_size);

              if (c!=INVALID_SOCKET )

              {

                     //为一个socketclient开启单独的接收线程

                     HANDLE h=::CreateThread(0,0, client_thread,(LPVOID)c,0,NULL);

                     ::CloseHandle(h);

              }

        

       }

 

       // 关闭套接字

       closesocket(sockfd);

 

       return 0;

}

 

先运行ThreadEchoServer.exe,再运行Client.exe 运行结果如下

 

 

 

现在,我们使用多线程实现了并发,有了可以同时和大量socket通讯的能力。

 

 

以上就是最简单的one thread one client。优点简单高效适合中小型网络通讯情景,很多程序都是这种并发模型,比如web服务器。缺点也很明显,一个客户就要消耗一个线程资源,当大量客户成百上千连接,那么服务器明显会性能下降,创建线程变慢,甚至线程无法创建。总之一句话,太浪费资源!还有另一个更重要的缺点,那就是线程并发模型,如果客户端之间相互都没有交集,不和其他客户通讯,只和服务器交流,那么线程并发模型会工作的性能良好,但如果客户之间有数据交换,麻烦就来了,线程间通讯,是一个难题,线程通信就要同步,那么线程同步,会导致更多麻烦,效率下降,代码易出错造成死锁。 那么我们就要考虑其他更加高效的解决办法了。那就是select io模型!

 

 

 

但是在开始学习之前,还要补一点概念知识。

1.5 阻塞,非阻塞,同步,异步

 

同步和异步  关注的是通信机制。

所谓同步,就是发出一个 ‘调用’ 时,在还没有获得结果前,此‘调用’ 就不返回。 同步听的比较多的应该是线程同步,也就是多线程操作一个公共数据,但是问题来了,多线程同时写一个区域,那么会导致混乱,所以引用了线程同步这个概念,以及同步相关技术,比如线程锁,信号量,互斥,事件。

所谓异步,就是发出一个‘调用’时,提供一个接收结果回调接口(callback)或其他通知结果到达的方法,调用就返回了。

 

经常见到一些程序说明,同步调用,异步调用,都是说他的程序通信机制。比如windows提供文件读写操作,就有同步函数和异步函数。同步函数调用简单,运行流程一直往下走就行。但异步调用就比较繁琐,运行流程比较复杂,非直线型。但异步往往效率会更高,因为不会有同步等待结果造成时间消耗。

 

阻塞和非阻塞 关注的是  调用中等待结果(消息,返回值)时的状态。

阻塞调用是指在阻塞模式下,在I / O操作完成前,执行操作的函数(比如s e n d和r e c v)会一直等候下去,不会立即返回程序,当前线程会被挂起。调用线程只有在得到结果之后才会返回。 

非阻塞调用指在非阻塞模式下,在不能立刻得到结果之前,调用不会阻塞当前线程,会立即返回。

 

看完了概念,我就来举一些例子说明。

accept 函数会阻塞线程,因为如果没有客户端连接监听的端口。那么accept就不会获得接受连接,会始终等待。

 

套接字有2种模式:阻塞非阻塞(有的书翻译成 锁定和非锁定)

accept/recv/send 等函数默认模式下是阻塞的,会阻塞线程。

 

对于处在锁定(阻塞)模式的套接字,我们必须多加留意,因为在一个锁定套接字上调用任何一个socket函数,都会产生相同的后果—耗费或长或短的时间“等待”。

 

相对的,非阻塞模式的套接字,调用会立即返回。大多数情况下,这些调用都会“失败”,并返回一个错误。

 

 

 

 

1.6 select io模型

Winsock提供的I/O模型一共有五种

select,WSAAsyncSelect,WSAEventSelect,Overlapped,CompletionPort。

Linux下提供的io模型也有几种,其中也有select。

这里先讲select io模型。因为它比较高效节能,至少比一客户一线程好多了,同时多平台都提供了select,学会了它就可以编写出跨平台的网络程序(当然socket本身就可以跨平台了)

 

 

之所以称其为“select模型”,是由于 它的“精髓”便是利用select函数,实现对I / O的管理!

 

 int select(
int nfds, 
fd_set FAR *readfds, 
fd_set FAR *writefds, 
fd_set FAR *exceptfds, 
const struct timeval FAR *timeout 
);

 

 

利用select函数,我们可以判断套接字上是否存在数据,或者能否向一个套接字写入数据。之 所以要设计这个函数,唯一的目的便是防止应用程序在套接字处于锁定模式中时,在一次 I / O调用(如send或recv)过程中,被迫进入“锁定”状态。

 

介绍下windows下的select参数含义。第一个nfds在windows里无用,默认0即可。

第二个参数表示fd_set读集,用于检查可读性(readfds),第三个参数用于检查可写性(writefds) ,第四个参数用于例外数据(exceptfds)。从根本上说, fd_set数据类型代表着一系列特定套接字的集合。可以跟踪进去查看

 

#ifndef FD_SETSIZE

#define FD_SETSIZE     64

#endif /* FD_SETSIZE */

 

typedef struct fd_set {

        u_int   fd_count;               /* howmany are SET? */

        SOCKET  fd_array[FD_SETSIZE];   /* an array ofSOCKETs */

} fd_set;

 

看见FD_SETSIZE默认最大64。即一次最大select调用检查 fd_set集合,可以传递64个socket句柄。当然我们可以在包含头文件之前先定义,比如#define FD_SETSIZE  1024

这样就可以一次检查1024个socket 读或写有效性。

 

 

最后一个参数timeout对应的是一个指针,它指向一个timeval结构,用于 决定select最多等待I / O操作完成多久的时间。如timeout是一个空指针,那么select调用会无限地“锁定”或停顿下去,直到至少有一个描述符符合指定的条件后结束。

 

 

 

Windows提供了下列宏操作,可用来针对I / O活动,对fd_set进行处理与检查:

■ FD_CLR(s, *set):从s e t中删除套接字s。

■ FD_ISSET(s, *set):检查s是否set集合的一名成员;是,则返回TRUE。

■ FD_SET(s, *set):将套接字s加入集合s e t。

■ FD_ZERO ( * s e t ):将s e t初始化成空集合。

 

 

 

用下述步骤,便 可完成用select操作一个或多个套接字句柄的全过程:

1.使用FD_ZERO,初始化一个fd_set。

2.使用FD_SET,将套接字句柄分配给一个fd_set。

3.调用select函数,然后等待在指定的fd_set集合中,I / O活动设置好一个或多个套接字句柄。 select 完成后,会返回在所有fd_set 集合中设置的套接字句柄总数,并对每个集合进行相应的更新。

4.根据select的返回值,我们的应用程序便可判断出哪些套接字存在着尚未完成(待决) 的I / O操作—具体的方法是使用FD_ISSET宏,对每个fd_set集合进行检查。

5.知道了每个集合中“待决”的I / O操作之后,对I / O进行处理,然后返回步骤1 ,继续进 行select处理。

 

     

// select模型处理过程 代码片段

 

    SOCKET sock;

       fd_set fdRead;    

    FD_ZERO(&fdRead);//FD_ZERO初始化套节字集合

       FD_SET(sock,&fdRead);// 添加要监听的套节字

// select检查集合

       int nRet = ::select(0,&fdRead, NULL, NULL, NULL);

       if(nRet > 0)

       {

      //测试sock是否有待决的I / O

if(FD_ISSET(sock, &fdRead))

          {

           //这里处理I / O

                  char szText[256];

                  int nRecv = ::recv(sock ,  szText, strlen(szText), 0);

               if(nRecv > 0)                                        //(2)可读

                      {

                        szText[nRecv]= '\0';

                            printf("接收到数据:%s \n", szText);

                   }

                   else     // (3)连接关闭

                      {

                             printf("%d断开 \n", sock );

                              ::closesocket(sock );

                          FD_CLR(sock ,  & fdRead);

                      }

}

}else

{

printf("Failed select() \n");

}

 

 

 

下面贴出使用select io模型实现的SelectEchoServer

 

SelectEchoServer.cpp

 

#include<stdio.h>

#include<winsock.h>

#pragma comment(lib,"Ws2_32")

#define MAXDATASIZE 100                          /* 每次可以接收的最大字节 */

 

      

int nPort = 800;   // 此服务器监听的端口号

 

int _tmain(int argc, _TCHAR* argv[])

{

       WSADATA ws;

       WSAStartup(MAKEWORD(2,2),&ws);     //初始化WindowsSocket Dll

 

 

 

       // 创建监听套节字

       SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, 0);    

       sockaddr_in sin;

       sin.sin_family = AF_INET;

       sin.sin_port = htons(nPort);

       sin.sin_addr.S_un.S_addr = INADDR_ANY;

       // 绑定套节字到本地机器

       if(::bind(sListen, (sockaddr*)&sin,sizeof(sin)) == SOCKET_ERROR)

       {

              printf("Failed bind() \n");

              return-1;

       }

       // 进入监听模式

       ::listen(sListen, 5);

 

       // select模型处理过程

       // 1)初始化一个套节字集合fdSocket,添加监听套节字句柄到这个集合

       fd_set fdSocket;        // 所有可用套节字集合

       FD_ZERO(&fdSocket);

       FD_SET(sListen,&fdSocket);

       while(TRUE)

       {

              // 2)将fdSocket集合的一个拷贝fdRead传递给select函数,

              // 当有事件发生时,select函数移除fdRead集合中没有未决I/O操作的套节字句柄,然后返回。

              fd_set fdRead = fdSocket;

              int nRet = ::select(0,&fdRead, NULL,NULL, NULL);

              if(nRet > 0)

              {

 

                     // 3)通过将原来fdSocket集合与select处理过的fdRead集合比较,

                     // 确定都有哪些套节字有未决I/O,并进一步处理这些I/O。

                     for(int i=0; i<(int)fdSocket.fd_count; i++)

                     {

                            if(FD_ISSET(fdSocket.fd_array[i], &fdRead))

                            {

                                   if(fdSocket.fd_array[i] == sListen)             // (1)监听套节字接收到新连接

                                   {

                                          if(fdSocket.fd_count< FD_SETSIZE)

                                          {

                                                 sockaddr_in addrRemote;

                                                 int nAddrLen = sizeof(addrRemote);

                                                 SOCKET sNew = ::accept(sListen, (SOCKADDR*)&addrRemote,&nAddrLen);

                                                 FD_SET(sNew, &fdSocket);

                                                 printf("接收到连接%d(%s)\n", sNew,::inet_ntoa(addrRemote.sin_addr));

                                          }

                                          else

                                          {

                                                 printf(" Too much connections! \n");

                                                 continue;

                                          }

                                   }

                                   else

                                   {

                                          char szText[256];

                                          int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);

                                          if(nRecv > 0)                                          // (2)可读

                                          {

                                                 szText[nRecv] ='\0';

                                                 printf("Received: [%s]\n",szText);

                                                 //echo

                                                 //echo

                                                 send(fdSocket.fd_array[i],  szText,  strlen(szText) , 0);

                                                 printf("send ok!\n");

                                          }

                                          else                                                  // (3)连接关闭

                                          {

                                                 printf("%d断开\n", fdSocket.fd_array[i]);

                                                 ::closesocket(fdSocket.fd_array[i]);

                                                 FD_CLR(fdSocket.fd_array[i], &fdSocket);

                                          }

                                   }

                            }

                     }

              }

              else

              {

                     printf(" Failed select() \n");

                     break;

              }

       }

 

 

       return 0;

}

 

 

先运行SelectEchoServer.exe ,再运行Client.exe,结果如下图

 

 

 

 

现在,我们很好的只在一个线程里就实现了并发,可以同时和大量socket通讯的能力。

 

 

1.7 iocp/boost asio

IOCP(I/O Completion Port),常称I/O完成端口。 IOCP模型属于一种通讯模型,适用于(能控制并发执行的)高负载服务器的一个技术,是windows上目前效率最高的I/O模型。使用IOCP可以实现成千上万的连接通讯,但是由于IOCP理解以及编码的复杂度较高,对使用者综合知识有一定要求,同步与异步,阻塞与非阻塞,重叠I/O技术,多线程,栈、队列,指针。对新手而言是个很大的障碍。于是我推荐使用Boost Asio。

 

Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一。

Asio则是Boost里的一个功能模块。提供了网络I/O功能。Asio提供了同步和异步的I/O功能,其中异步I/O(在windows平台下)的底层最终调用的就是IOCP。

我们如果学会了Asio,就等于学会了高效的网络编程,而且是跨平台的。你需要的只是下载,然后花一点时间学会它。

 

Boost下载安装就不细说了。官网http://www.boost.org/

目前最新的版本是  1.59  ,我开发用的版本是1.46

 

 

还是以echo功能作为例子,用asio 阻塞模式的socket来实现。

 

AsioBlockEchoServer.cpp

 

#include <cstdlib>

#include <iostream>

#include <boost/bind.hpp>

#include <boost/smart_ptr.hpp>

#include <boost/asio.hpp>

#include <boost/thread.hpp>

 

using boost::asio::ip::tcp;

 

const int max_length = 1024;

 

typedef boost::shared_ptr<tcp::socket> socket_ptr;

 

void session(socket_ptr sock)

{

       try

       {

              for (;;)

              {

                     char data[max_length];

 

                     boost::system::error_code error;

                     size_t length = sock->read_some(boost::asio::buffer(data), error);

                     if (error == boost::asio::error::eof)

                            break;// Connection closedcleanly by peer.

                     else if (error)

                            throw boost::system::system_error(error);// Some othererror.

 

                     boost::asio::write(*sock, boost::asio::buffer(data, length));

              }

       }

       catch (std::exception& e)

       {

              std::cerr <<"Exception in thread: " << e.what() << "\n";

       }

}

 

void server(boost::asio::io_service& io_service,short port)

{

       tcp::acceptora(io_service,tcp::endpoint(tcp::v4(), port));

       for (;;)

       {

              socket_ptr sock(new tcp::socket(io_service));

              a.accept(*sock);

              boost::thread t(boost::bind(session, sock));

       }

}

 

int main(int argc,char* argv[])

{

       try

       {

              boost::asio::io_service io_service;

              server(io_service,  800 );

       }

       catch (std::exception& e)

       {

              std::cerr <<"Exception: " << e.what() << "\n";

       }

 

       return 0;

}

 

 

可以看出boost的封装代码风格,纯面向对象,全部小写,用到了bind,shared_ptr,这些高级的C++技术,对新手来说还是有些吃力。

这个例子是阻塞模式,接受一个连接就开启一个线程的。跟我们前面的一线程一客户的io模型是一个原理。

 

 

下面是异步asio socket实现的Echo Server

 

AsioAsyncEchoServer.cpp

 

#include <cstdlib>

#include <iostream>

#include <boost/bind.hpp>

#include <boost/asio.hpp>

 

using boost::asio::ip::tcp;

 

class session

{

public:

       session(boost::asio::io_service& io_service)

              : socket_(io_service)

       {

       }

 

       tcp::socket&socket()

       {

              return socket_;

       }

 

       void start()

       {

              socket_.async_read_some(boost::asio::buffer(data_, max_length),

                     boost::bind(&session::handle_read, this,

                     boost::asio::placeholders::error,

                     boost::asio::placeholders::bytes_transferred));

       }

 

       void handle_read(const boost::system::error_code&error,

              size_t bytes_transferred)

       {

              if (!error)

              {

                     boost::asio::async_write(socket_,

                            boost::asio::buffer(data_, bytes_transferred),

                            boost::bind(&session::handle_write,this,

                            boost::asio::placeholders::error));

              }

              else

              {

                     delete this;

              }

       }

 

       void handle_write(const boost::system::error_code&error)

       {

              if (!error)

              {

                     socket_.async_read_some(boost::asio::buffer(data_, max_length),

                            boost::bind(&session::handle_read,this,

                            boost::asio::placeholders::error,

                            boost::asio::placeholders::bytes_transferred));

              }

              else

              {

                     delete this;

              }

       }

 

private:

       tcp::socketsocket_;

       enum { max_length = 1024 };

       char data_[max_length];

};

 

class server

{

public:

       server(boost::asio::io_service& io_service,short port)

              : io_service_(io_service),

              acceptor_(io_service,tcp::endpoint(tcp::v4(), port))

       {

              session* new_session= new session(io_service_);

              acceptor_.async_accept(new_session->socket(),

                     boost::bind(&server::handle_accept, this,new_session,

                     boost::asio::placeholders::error));

       }

 

       void handle_accept(session* new_session,

              const boost::system::error_code&error)

       {

              if (!error)

              {

                     new_session->start();

                     new_session= new session(io_service_);

                     acceptor_.async_accept(new_session->socket(),

                            boost::bind(&server::handle_accept,this, new_session,

                            boost::asio::placeholders::error));

              }

              else

              {

                     delete new_session;

              }

       }

 

private:

       boost::asio::io_service&io_service_;

       tcp::acceptoracceptor_;

};

 

int main(int argc,char* argv[])

{

       try

       {

              boost::asio::io_service io_service;

              server s(io_service, 800 );

              io_service.run();

       }

       catch (std::exception& e)

       {

              std::cerr <<"Exception: " << e.what() << "\n";

       }

 

       return 0;

}

 

 

1.8 asio基本概念

asio基于两个概念:I/O服务和I/O对象

 

 

I/O服务,抽象了操作系统的异步接口  这个对象是核心

boost::asio::io_service

 

I/O对象,有多种对象

boost::asio::ip::tcp::socket   socket的oop封装

boost::asio::ip::tcp::resolver

boost::asio::ip::tcp::acceptor接受器。功能就是accept客户的connect请求

boost::asio::local::stream_protocol::socket本地连接

boost::asio::posix::stream_descriptor面向流的文件描述符,比如stdout, stdin

boost::asio::deadline_timer定时器

boost::asio::signal_set信号处理

 

所有 I/O 对象通常都需要一个I/O 服务作为它们的构造函数的第一个参数,比如:

boost::asio::io_serviceio_service;

boost::asio::deadline_timertimer(io_service, boost::posix_time::seconds(5));

 

 

开始分析AsioAsyncEchoServer代码,首先定义了一个io_service,然后创建了server对象的实例,server构造传递了io_service和一个端口号,构造初始化列表又初始化了acceptor_对象,用于接受连接,

acceptor_的构造传递了io_service,和tcp::endpoint(tcp::v4(), port) 用于创建一个ipv4上的监听端口。接着new了一个 session (表示一个连接/会话),然后acceptor_.async_accept把这个session投递,并传递handle_accept用于实现async_accept事件完成后的回调。这样后,当客户连接了Server的800端口,Server即会调用handle_accept来处理连接请求。handle_accept里的处理也很简单,如果没出错,参数new_session调用start开始预读数据,接着创建一个新的session用acceptor_.async_accept投递,从而达到不断接受新连接,如果handle_accept出错了,则delete这个会话对象。

async_read_some  async_write 这两个异步读写方法,分别使用handle_read handle_write来处理I/O完成的结果。handle_read代码是

如果没出错,则说明上次投递的读操作成功了,然后使用async_write把成功读取的数据再发送出去,出错了则delete会话自己。

1.9数据协议 打包

看到这里,有的同学会问了,这些代码虽然都看懂了,但只是Echo发送接收字符串数据太简单了,想知道那些复杂的服务器发送各种消息数据是怎么实现的? 别急,这一节就开始讲解数据协议与打包。

 

首先,网络服务端程序需要制定一个协议,规范,有了数据协议,才能双方通讯正常,比如web server遵守了http协议,浏览器才能按http协议正常的访问。

 

发送的数据并不局限于字符串,其实也可以发送任意数据类型,intfloat数据类型 甚至是结构体,因为它们最终会被转化为二进制流传输。

 

比如

float f;

int ret= recv( s, (char*)&f, sizeof(float) ,0 );

这样就可以接收一个float类型的数据

 

send(s, (char*)&f, sizeof(float),0 ); 这样就可以发送一个float类型的数据

 

然后按顺序发送按顺序接收各种类型的数据,就是所谓的数据打包解包。

 

下面总结一下常见的数据打包方法

1.       C结构体大法,发送接收结构体,免去了数据打包解包。

2.       XML文件法,把数据序列化成xml文件字符串然后发送,另一端接收然后字符串解析成xml格式文件,读取xml数据。略微繁琐,数据是可视化字符,传输二进制类型时要转码,空间会增大,但避免了结构体类型的依赖,可以更广泛的支持其他平台。

3.       Json文件法,类型xml文件法。只是格式换成了json,比xml节约空间。

4.       顺序写顺序读(序列化)法,即每个消息都要定义它的打包解包的具体数据读写顺序。略微繁琐。

 

方法各有各的优点,比如结构体法,如果写的程序只考虑c/c++,那么结构体最简便。

 

接下来贴出一个结构体Server/Client的例子,让大家来学习。

 

Struct_Server.cpp

 

#include<stdio.h>

#include<winsock.h>

#pragma comment(lib,"Ws2_32")

 

struct MSG1

{

       int cmd;

       float f;

       char msg[50];

};

 

 

int _tmain(int argc, _TCHAR* argv[])

{

       int sockfd, new_fd;                       /*    定义套接字        */

       struct sockaddr_in my_addr;         /*    本地地址信息    */

       struct sockaddr_in their_addr;/*    连接者地址信息       */

       int sin_size;

 

       WSADATA ws;

       WSAStartup(MAKEWORD(2,2),&ws);     //初始化WindowsSocket Dll

 

       //建立socket

       sockfd = socket(AF_INET, SOCK_STREAM,0);

 

       //bind本机的端口

       my_addr.sin_family= AF_INET;                /* 协议类型是INET */

       my_addr.sin_port = htons(800);            /* 绑定端口     */

       my_addr.sin_addr.s_addr = INADDR_ANY;   /* 本机IP                  */

 

       bind(sockfd, (struct sockaddr*)&my_addr,sizeof(struct sockaddr)); 

 

       //listen,监听端口

       listen(sockfd, 5);

       printf("listen......\n"); 

       //等待客户端连接

       sin_size = sizeof(struct sockaddr_in);

       new_fd = accept(sockfd, (struct sockaddr *)&their_addr,&sin_size);

 

 

       //接收数据并打印出来

       MSG1 m;

       int numbytes=recv(new_fd, (char*)&m,sizeof(MSG1), 0);

        

       printf("MSG1.cmd= %d \n",m.cmd);

       printf("MSG1.f= %f \n",m.f);

       printf("MSG1.msg= %s \n",m.msg);

 

       //echo

       send(new_fd,  (char*)&m, sizeof(MSG1), 0);

       printf("send ok!\n");

 

 

       // 关闭套接字

       closesocket(sockfd);

       closesocket(new_fd);

 

       return 0;

}

 

 

 

 

Struct_Client.cpp

 

#include<stdio.h>

#include<stdio.h>

#include<winsock.h>

#pragma comment(lib,"Ws2_32")

 

#define SERVER_IP "127.0.0.1"

#define SERVER_PORT 800

 

 

struct MSG1

{

       int cmd;

       float f;

       char msg[50];

};

 

int _tmain(int argc, _TCHAR* argv[])

{

 

       WSADATA ws;

       WSAStartup(MAKEWORD(2,2),&ws);     //初始化WindowsSocket Dll

 

       int sockfd = socket(AF_INET, SOCK_STREAM, 0);

 

       sockaddr_in their_addr;    /* 对方的地址端口信息 */

       //连接对方

       their_addr.sin_family= AF_INET;                                  /* 协议类型是INET */

       their_addr.sin_port= htons(SERVER_PORT);                             /* 连接对方800端口       */

       their_addr.sin_addr.s_addr = inet_addr(SERVER_IP ); /* 连接对方的IP      */

       connect(sockfd, (struct sockaddr*)&their_addr,sizeof(struct sockaddr));

 

       //发送结构体给服务器

       MSG1 m;

       m.cmd=123;

       m.f=3.1415;

       strcpy(m.msg,"hello world!");

       send(sockfd,  (char*)&m, sizeof(MSG1), 0);

       printf("send ok!\n");

 

 

       //接收数据并打印出来

       MSG1 m2;

       int numbytes=recv(sockfd, (char*)&m2,sizeof(MSG1), 0);

 

       printf("MSG1.cmd= %d \n",m2.cmd);

       printf("MSG1.f= %f \n",m2.f);

       printf("MSG1.msg= %s \n",m2.msg);

 

       //关闭套接字

       closesocket(sockfd);

       return 0;

}

 

 

 

 

 

 

 

如图所示,Server和Client完美的使用了结构体通讯成功,Server接收并打印出结构体的成员数据,然后echo结构体给Client。

 

以此类推,设计好各种收发协议,定义好各种命令id,结构体成员数据,通常首个成员定义成int cmd;来表示命令id。

 

 

1.10网络引擎 模块化

结合前面所有知识的学习,我们这一节开始把网络模块封装化,成为一个独立的网络引擎,封装成引擎的好处有很多,把网络io代码和实际业务代码分离,不再强耦合,把代码模块化,定义好接口,即插即用。

 

我采用的是函数指针回调方式。

 

 

/*******************************************************************/

/*

  服务端网络引擎接口

 

*/

/*******************************************************************/

//回调处理数据函数原型

typedef VOID WINAPIServerProcessRecvData( DWORD dwNetworkIndex ,  BYTE *pMsg ,  WORD wSize  );

 

class INetServer

{

public: 

       //是否已初始化监听

       virtualBOOL IsListening()=0;

 

       //网络初始化

       virtual BOOL Init(  char* IP, WORD Port , ServerProcessRecvData* pProcessRecvData ,   DWORD MaxConnectNum )=0;

 

       //停止网络服务

       virtual VOID Shutdown()=0;

 

       //更新

       virtual VOID Update()=0;

 

       //单个断开

       virtual BOOL DisConnect( DWORDdwNetworkIndex )=0;

 

       //单个发送

       virtual BOOL Send( DWORD dwNetworkIndex ,BYTE *pMsg , WORD wSize )=0;

 

       //得到当前总连接数

       virtual DWORD GetNumberOfConnections()=0;

 

       //得到ip

       virtual char* GetIP(   DWORD dwNetworkIndex )=0;

 

};

 

 

INetServer就是我们服务端网络引擎的接口定义。

 

ServerProcessRecvData函数指针的参数的分别意义是Client会话索引,接收到的数据,接受到的数据长度。如果接收到的数据指针为空,则表示连接断开了。

 

 

 

下面是客户端网络引擎的接口定义

/*******************************************************************/

/*

   客户端网络引擎接口

 

*/

/*******************************************************************/

//回调处理数据函数原型

typedef VOID WINAPIClientProcessRecvData(INetClient* p, BYTE *pMsg ,   WORD wSize );

 

class INetClient

{

 

public:

       //连接服务器

       virtual BOOL Connect(  char* ServerIP,  WORD Port , ClientProcessRecvData* pProcessRecvData )=0;

 

       //断开连接

       virtual BOOL DisConnect( )=0;

 

       //指定发送

       virtual BOOL Send(  BYTE *pMsg ,  WORD wSize )=0;

      

       //更新

       virtual VOID Update()=0;

 

};

 

1.11网络引擎内部数据协议

为了网络引擎运行良好,我们还需要设置一个协议规范。比如有时接收数据,我们并不知道对方发送什么类型的数据,也不知道后续数据还有多长?怎么解决这个问题?答案就是要制定一个数据协议,我们规定网络引擎接口Send函数不仅要发送用户指定长度的数据,还要保证数据完整发送,另一端接收数据保证完整接收,要保证回调处理函数获得的数据无误,且知道这一次回调的数据长度大小。

 

我们需要在Send函数内部把用户数据封包,好比发快递包裹,需要给包贴上标签标明包的一些数据信息,Send把用户数据封包,这个用户数据通常称之为‘包体’,而把用户数据额外信息的这一部分称之为‘包头’, Send的数据被打包成  ‘包头’ +‘包体’,然后发送出去。包头里最重要的信息当然是标明包体长度了,不然接收端不知道后续数据到底有多大。

 

包头通常用结构体表示。如下就是一个包头的定义

 

struct PACKET_HEADER

{

       WORD size; //待接收的数据包总大小

};

 

这样有了包头,我们网络引擎就能完美的解决数据包长度的相关问题。

 

ps:你也可以扩展包头的定义,比如加入校验位,crc32数据包校验,加密key,版本号等等

 

Send的函数实现如下

 

 

BOOL Send (DWORD dw, BYTE *pMsg , WORD wSize )

 

       //包头

       PACKET_HEADER pack_hdr;

       pack_hdr.size=wSize;

 

       //发送包头

       int rt=send(dw, (char*)&pack_hdr,sizeof(PACKET_HEADER),0 );

       if(rt == SOCKET_ERROR || rt ==0)  returnFALSE;

 

      //发送包体

rt=send(dw,(const char*) pMsg ,wSize,0);

if(rt== SOCKET_ERROR || rt== 0)  returnFALSE;

        

       return TRUE;

 

}

 

 

接收数据包时 思路就清晰了,先接收包头,得到包体长度,再接收包体,这样一包数据就接受成功了。

 

 

1.12网络引擎

接下来我们开始网络引擎的封装代码编写工作。首先是基于select io模型的网络引擎编写。

 

NetServer项目的代码结构如下

 

SelectIOServer就是INetServer网络接口的实现类。实现了诸如Init, Update ,Send

这些重要的网络方法。

Selector是select io模型的封装。简化操作。

Session是客户端对象的封装类,每一个Session就表示一个socket客户端对象。

SockInit是一个初始化winsock 库的类,很简单。

typedef std::list<Session*> SESSION_LIST;是一个容器,链表,用于存放我们Session对象。

 

PACKET_HEADER是我们包头的定义。

 

下面贴出main.cpp 使用网络引擎NetServer的例子

 

 

SelectIOServer s;

 

struct STest

{

       int cmd;

       byte cc;

       int a;

       DWORD time;

       char name[100];

       char name2[10];

};

 

 

int sendcount=0;

int lastsendcount=0;

int recvcount=0;

int lastrecvcount=0;

 

//网络消息处理

void WINAPI RecvDataCall(DWORD dwNetworkIndex, BYTE *pMsg,WORD wSize)

{

       if( wSize == 0 || pMsg == NULL )//客户退出了

       {

              printf("[%d]退出\n",dwNetworkIndex );

              return;

       }else{

 

              ++recvcount;

              s.Send(dwNetworkIndex,pMsg,wSize);//简单echo

              ++sendcount;

       }

}

//-------------------------------------------------------------------------------------------------

// Main

//-------------------------------------------------------------------------------------------------

 

int _tmain(int argc, _TCHAR* argv[])

{

       int port=123;

 

       if(s.Init("",port,RecvDataCall,999))

       {

              printf("CreateNetServer   ok! listen [%d] ok\n",port);

       }else{

              printf("CreateNetServer   err! listen err\n");

              return 0;

       }

 

       printf("按任意键退出\n");

 

       DWORD last=::GetTickCount();

       while(s.IsListening() )

       {

              if (kbhit())

              {

                     getch();

                     // bd();

                     break;

              }

 

              s.Update();

 

              if(GetTickCount()-last>1000)

              {

                     last=GetTickCount();

 

                     printf("有[%d]个客户连接  发包[%d/s]收包[%d/s]\n",s.GetNumberOfConnections()

 

                            ,sendcount,recvcount );

 

                     lastrecvcount=recvcount;

                     lastsendcount=sendcount;

                     sendcount=0;

                     recvcount=0;

              }

 

              Sleep(1);

 

       }

 

 

       return 0;

}

 

 

NetClient更加简单就不多做介绍了。

 

 

 

 

NetClient和NetServer 主要的网络更新工作都在Update函数里进行。

Update 里就是使用select函数测试当前sock是否可读,如果可读,就对应接收数据,然后回调给接口ClientProcessRecvData或ServerProcessRecvData。

 

 

 

NetClient和NetServer两个工程代码编写完毕,先运行netserver.exe ,再运行netclient.exe 会看见双方开始收发数据了。如图所示

 

 

 

 

 

1.13 本章总结

至此,我们本章的内容就讲完了。我们实现了一个网络模块。但是需要完善优化的还有很多地方。比如异常的处理,数据有效性检查,还有经常有人提到的“粘包”问题,这些就留给大家去研究,去自我提高吧。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2016.1.3                           作者:司马威

作者博客      http://blog.csdn.net/smwhotjay

 

编程交流q群: 316641007

 

转载于:https://my.oschina.net/simawei/blog/1526817

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值