admin 管理员组

文章数量: 887021


2023年12月22日发(作者:microsoft help)

Socket编程入门教程作者:龙飞整理:ltsmat摘自:/lf426/category/?Show=All

目录目录...................................................................................................................................................2TCPserver端....................................................................................................................................31、建模.....................................................................................................................................32、socket与文件描述符..........................................................................................................43、sockaddr与54、构造函数涉及的概念.........................................................................................................85、创建监听嵌套字...............................................................................................................116、创建“通讯”嵌套字..........................................................................................................137、接收与发送.......................................................................................................................158、本章的完整源代码...........................................................................................................17win32下使用20winsock演示程序:25TCP原理.........................................................................................................................................321、socket异常信息................................................................................................................322、设计TCPsocket的类(上)...........................................................................................353、设计TCPsocket的类(中)...........................................................................................374、设计TCPsocket的类(下)...........................................................................................395、TCP的三次握手(three-wayhandshake).....................................................................416、字节流的发送与接收.......................................................................................................427、TCP连接的关闭...............................................................................................................44TCP应用.........................................................................................................................................451、构建echo服务器..............................................................................................................452、构建echo客户端..............................................................................................................47UDP原理........................................................................................................................................501、设计UDPserver类..........................................................................................................502、设计UDPclient类...........................................................................................................533、UDP的系统缓存队列......................................................................................................554、“有连接”的565、预读57UDP应用........................................................................................................................................581、UDP版的582、UDP版的60

TCPserver端1、建模绝大部分关于socket编程的教程总是从socket的概念开始讲起的。要知道,socket的初衷是个庞大的体系,TCP/IP只是这个庞大体系下一个很小的子集,而我们真正能用上的更是这个子集中的一小部分:运输层(Host-to-HostTransportLayer)的TCP和UDP协议,以及使用这两个协议进行应用层(ApplicationLayer)的开发。即使是socket的核心部分,网络层(InternetLayer)的IP协议,在编程的时候我们也很少会感觉到它的存在——因为已经被封装好了,我们唯一需要做的事情就是传入一个宏。第一节我想介绍的概念就这么多,当然,既然我们已经说了3个层了,我想最好还是把最后一个层也说出来,即所谓链路层(NetworkAccessLayer),它包括了物理硬件和驱动程序。这四个层从底到高的顺序是:链路层--网络层--运输层--应用层。好,说实话我们现在并不清楚所谓TCP到底是什么东东,不过我们知道这东东名气很大。或许你早就知道,另外一个声名狼藉建立在TCP协议基础上的应用程序,它曾经几乎是统治了一个时代,即使是今天,我们依然无法消除他的影响力的——恩,是的,就是telnet。在这个教程中,我使用的环境是DebianGNU/Linux4.0etch。传说中的stable-_-!!!,恩,我是很保守的人。如果你不是自己DIY出来的系统,相信默认安装里面就应该有telnet(/usr/bin/telnet,要是没装就自己aptitudeinstall吧)。telnet可以与所有遵循TCP协议的服务器端进行通讯。通常,socket编程总是Client/Server形式的,因为有了telnet,我们可以先不考虑client的程序,我们先写一个支持TCP协议的server端,然后用telnet作为client验证我们的程序就好了。server端的功能,我们也考虑一种最简单的反馈形式:echo。就如同你在终端输入echo'HelloWorld',回车后shell就会给你返回HelloWorld一样,我们的第一个TCPserver就用以实现这个功能。什么样的模型适合描述这样的一种server呢?我相信,一个很2的例子会有助于我们记忆TCPserver端的基本流程。想象你自己是个小大佬,坐办公室(什么样的黑社会做办公室啊?可能是讨债公司吧^^)你很土,只有一个小弟帮你接电话(因为你自己的号码是不敢对外公开的)。一次通讯的流程大概应该是这样的:小弟那里的总机电话响了;小弟接起电话;对方说是你女朋友A妹;小弟转达说,“老大,你马子电话”;你说,接过来;小弟把电话接给你;你和你女朋友聊天半小时;挂电话。我们来分析一下整个过程中的元素。先分析成员数据(请注意,这里开始用C++术语了):你小弟(listenSock),你需要他来监听(listen,这是socket编程中的术语)电话;你自己(communicationSock),实际上打电话进行交流的是你自己;你的电话号码(servAddr),否则你女朋友怎么能找到你?你女朋友的电话号码(clntAddr),这个比喻有点牵强,因为事实上你接起电话,不需要知道对方的号码也可以通话(虽然事实上你应该是知道的,你不会取消了来电显示功能吧^^),但是,难道你是只接女朋友电话从来不打过去的牛人吗?这个过程中的行为(成员函数):你小弟接电话并转接给你(isAccept());你自己的通话(handleEcho())(这个行为确实比较土,只会乌鸦学舌的echo,呵呵)。

简单的说,就是这些了。根据这个模型,我们可以很容易写出实现我们需要的echo功能的TCPserver的类:classTcpServer{private:intlistenSock;intcommunicationSock;sockaddr_inservAddr;sockaddr_inclntAddr;public:TcpServer(intlisten_port);boolisAccept();voidhandleEcho();};这里面有些简写,比如,sock实际上就是socket,addr就是address。serv和clnt我想你一定能猜到是server和client吧。还有一个socket中的结构体sockaddr_in,实际上就是这个意思:socketaddressinternet(网络嵌套字地址),具体解说,请看下回分解。2、socket与文件描述符UNIX中的一切事物都是文件(everythinginUnixisafile!)当我在这篇教程中提到UNIX的时候,其意思专指符合UNIX标准的所谓“正统”UNIX的衍生系统(其实我就用来带指那些买了最初UNIX源代码的商业系统)操作系统和类似Linux,BSD这些类UNIX系统。如果某些要点是Linux特有的,或者因为本人孤陋寡闻暂时搞不清楚是Linux特有的还是UNIX通用的,我就会指明是Linux,甚至其发行版(我本人在写这篇教程的时候是以DebianGNU/Linux4.0etch为测试平台的)。我们学习UNIX的时候,恐怕听到的第一句话就是这句:UNIX中一切都是文件。这是UNIX的基本理念之一,也是一句很好的概括。比如,很多UNIX老鸟会举出个例子来,“你看,/dev/hdc是个文件,它实际上也是我的光盘……”UNIX中的文件可以是:网络连接(networkconnection),输入输出(FIFO),管道(apipe),终端(terminal),硬盘上的实际文件,或者其它任何东东。文件与文件描述符(file&filedescriptor)你可能对上一章中建模类中的int还记忆犹新。我们用int在描述socket,实际上,所有的文件描述符都是int,没错,用的是一个整数类型。如果你觉得这样让你很难接受,那么恭喜你,你跟我一样,也许是深中C++面向对象思想的毒了^^。因为是int,所以文件描述符不可能是C++概念中的对象,因为int无法发出行为,但是,这并不代表也不能接受一个动作哈。PASCAL之父在批判面向对象思想教条的时候,曾经生动的举了个例子,“在OOP的概

念中,绝对不应该接受a+b这种表达的,OOP对这个问题的表达应该是(b)”。fd(filedescriptor)可以作为接受动作的对象,但是本身却无法发出动作,这就如同一个只能做宾语不能做主语的名词,是个不完整的对象。但是,请别忘了Linux和socket本身是C语言的产物,我们必须接受在面向过程时代下的产物,正视历史——当然,这与我们自己再进行OOP的封装并不矛盾。我们应该记住3个已经打开的fd,0:标准输入(STDIN_FILENO);1:标准输出(STDOUT_FILENO);2:标准错误(STDERR_FILENO)。(以上宏定义在中)一个最简单的使用fd的例子,就是使用中的函数:write(1,"Hello,World!n",20);,在标准输出上显示“Hello,World!”。另外一个需要注意的问题是,file和fd并非一定是一一对应的。当一个file被多个程序调用的时候,会生成相互独立的fd。这个概念可以类比于C++中的引用(eg:int&rTmp=tmp;)。socket与filedescriptor文件是应用程序与系统(包括特定硬件设备)之间的桥梁,而文件描述符就是应用程序使用这个“桥梁”的接口。在需要的时候,应用程序会向系统申请一个文件,然后将文件的描述符返回供程序使用。返回socket的文件通常被创建在/tmp或者/usr/tmp中。我们实际上不用关心这些文件,仅仅能够利用返回的socket描述符就可以了。好了,说了这么多,实际上就解释了一个问题,“为什么socket的类型是int?”-_-!!3、sockaddr与sockaddr_in收件人地址一家化妆品公司将一批新产品的样品,准备发给某学校某个班的女生们免费试用。通常情况下,这件邮包的地址上可以这么写:收件人:全体女生。地址:A省B市C学校,X级Y班。但是,如果在描述地址的时候这样写呢:收件人:全体女生。地址:请打电话xxxxxxxx,找他们学校一个叫Lucy的女生,然后把东西送到她的班上。这种文字是相当的诡异啊-_-!!!,但是并不等于就没有表述清楚邮包的去向和地址。事实上邮局看到这样的地址一定会发飙的,然而对于电脑,如果你的地址描述形式是他可以接受和执行的,他就会老老实实的按你的要求去做……所以,如何描述地址不是问题的关键,关键在于这样的表述是不是能够表述清楚一个地址。一种更加通用的表达形式可能是这样的:收件人:全体女生。

地址:<一种地址描述方式>事实上,在socket的通用address描述结构sockaddr中正是用这样的方式来进行地址描述的:structsockaddr{unsignedshortsa_family;charsa_data[14];};这是一个16字节大小的结构(2+14),sa_family可以认为是socketaddressfamily的缩写,也可能被简写成AF(AddressFamily),他就好像我们例子中那个“收件人:全体女生”一样,虽然事实上有很多AF的种类,但是我们这个教程中只用得上大名鼎鼎的internet家族AF_INET。另外的14字节是用来描述地址的。这是一种通用结构,事实上,当我们指定sa_family=AF_INET之后,sa_data的形式也就被固定了下来:最前端的2字节用于记录16位的端口,紧接着的4字节用于记录32位的IP地址,最后的8字节清空为零。这就是我们实际在构造sockaddr时候用到的结构sockaddr_in(意指socketaddressinternet):structsockaddr_in{unsignedshortsin_family;unsignedshortsin_port;structin_addrsin_addr;charsin_zero[8];};我想,sin_的意思,就是socket(address)internet吧,只不过把address省略掉了。sin_addr被定义成了一个结构,这个结构实际上就是:structin_addr{unsignedlongs_addr;};in_addr显然是internetaddress了,s_addr是什么意思呢?说实话我没猜出值得肯定的答案(根据下面网友的评论,其意思为sourceaddress,谢谢),也许就是socketaddress的意思吧,尽管跟更广义的sockaddr结构意思有所重复了。哎,这些都是历史原因,也许我是没有精力去考究了。sockaddr和sockaddr_in在Linux中的实现你可能还记得我之前说过,UNIX和Linux上的socket实现都是从BSD的socket实现演变过来的。事实上,socket这个词本来的意思,就是BerkeleySocketinterface的简单说

法。Linux上的socket与原本的socket的应该是完全兼容的,不过发展到今天,在代码实现上可能有些小的差别。我们就吹毛求疵的来看看这些区别在什么地方。#include/*sockaddr{__SOCKADDR_COMMON(sa_);*/charsa_data[14];};//==============/*POSIX.1gspecifiesthistypenameforthe`sa_family'member.*/typedefunsignedshortintsa_family_t;/*Thismacroisusedtodeclaretheinitialcommonmembersofthedatatypesusedforsocketaddresses,`structsockaddr',`structsockaddr_in',`structsockaddr_un',etc.#define__SOCKADDR_COMMON(sa_prefix)*//*Addressdata.*//*Commondata:addressfamilyandlength.*/sa_family_tsa_prefix##family#define__SOCKADDR_COMMON_SIZE(sizeof(unsignedshortint))可以看到,转了几次typedef,几次宏定义,实际效果是与标准socket一样的。#include/*in_addr{in_addr_ts_addr;};//=================/*sockaddr_in{__SOCKADDR_COMMON(sin_);in_port_tsin_port;/*Portnumber.*/*/*/typedefuint32_tin_addr_t;

structin_addrsin_addr;/*Internetaddress.*/*//*Padtosizeof`structsockaddr'.unsignedcharsin_zero[sizeof(structsockaddr)-__SOCKADDR_COMMON_SIZE-sizeof(in_port_t)-sizeof(structin_addr)];};同样的,看起来挺复杂,实际上与标准socket的定义是一样的。头文件依赖关系是包含在中的,是包含在中的,实际上我们在程序中往往就是:#include#include值得知道的是,ARPA是Advancedresearchprojectagency(美国国防部高级研究计划暑)的所写,ARPANET是当今互联网的前身,所以我们就可以想象,为什么inet.h会在arpa目录下了。4、构造函数涉及的概念话题回到“黑社会办公室”的例子,讲概念已经扯得比较远了,不过,这一节我们还得讲概念,不过好在有些程序的例子。如果大家不想翻回去看TcpServer类的原型,我这里直接给出这个头文件的完整源代码://Filename:#ifndefTCPSERVERCLASS_HPP_INCLUDED#defineTCPSERVERCLASS_HPP_INCLUDED#include#include#include#includeclassTcpServer{private:intlistenSock;intcommunicationSock;

sockaddr_inservAddr;sockaddr_inclntAddr;public:TcpServer(intlisten_port);boolisAccept();voidhandleEcho();};#endif//TCPSERVERCLASS_HPP_INCLUDED我们已经解释了为什么listenSock和communicationSock的类型是int,以及sockaddr_in是什么结构,现在来写这个类的构造函数:TcpServer::TcpServer(intlisten_port){if((listenSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0){throw"socket()failed";}memset(&servAddr,0,sizeof(servAddr));_family=AF_INET;_addr.s_addr=htonl(INADDR_ANY);_port=htons(listen_port);if(bind(listenSock,(sockaddr*)&servAddr,sizeof(servAddr))<0){throw"bind()failed";}if(listen(listenSock,10)<0){throw"listen()failed";}}好,先看看程序培养一下感觉,我们还得说概念。数据封装(DataEncapsutation)我们前面说到了网络分层:链路——网络——传输——应用。数据从应用程序里诞生,传送到互联网上每一层都会进行一次封装:Data>>Application>>TCP/UDP>>IP>>OS(Driver,Kernel&PhysicalAddress)我们用socket重点描述的是协议,包括网络协议(IP)和传输协议(TCP/UDP)。sockaddr重点描述的是地址,包括IP地址和TCP/UDP端口。socket()函数

我们从TcpServer::TcpServer()函数可以看到,socket和sockaddr的产生是可以相互独立的。socket()的函数原型是:intsocket(intprotocolFamily,inttype,intprotocol);在Linux中的实现为:#include/*CreateanewsocketoftypeTYPEindomainDOMAIN,OCOLiszero,oneischosenautomatically.*/Returnsafiledescriptorforthenewsocket,intsocket(int__domain,int__type,int__protocol)__THROW;第一个参数是协议簇(Linux里面叫作域,意思一样的),还是那句话,我们这篇教程用到的就仅仅是一个PF_INET(protocolfamily:internet),很多时候你会发现人们也经常在这里赋值为AF_INET,事实上,当前,AF_INET就是PF_INET的一个#define,但是,写成PF_INET从语义上会更加严谨。这也就是TCP/IP协议簇中的IP协议(InternetProtocol),网络层的协议。后面两个参数定义传输层的协议。第二个参数是传输层协议类型,我们教程里用到的宏,只有两个:SOCK_STREAM(数据流格式)和SOCK_DGRAM(数据报格式);(具体是什么我们以后讨论)第三个参数是具体的传输层协议。当赋值为0的时候,系统会根据传输层协议类型自动匹配和选择。事实上,当前,匹配SOCK_STREAM的就是TCP协议;而匹配SOCK_DGRAM就是UDP协议。所以,我们指定了第二个参数,第三个就可以简单的设置为0。不过,为了严谨,我们最好还是把具体协议写出来,比如,我们的例子中的TCP协议的宏名称:IPPROTO_TCP。数据的“地址”从数据封装的模型,我们可以看到数据是怎么从应用程序传递到互联网的。我们说过,数据的传送是通过socket进行的。但是socket只描述了协议类型。要让数据正确的传送到某个地方,必须添加那个地方的sockaddr地址;同样,要能接受网络上的数据,必须有自己的sockaddr地址。可见,在网络上传送的数据包,是socket和sockaddr共同“染指”的结果。他们共同封装和指定了一个数据包的网络协议(IP)和IP地址,传输协议(TCP/UDP)和端口号。网络字节和本机字节的相互转换sockaddr结构中的IP地址(sin_addr.s_addr)和端口号(sin_port)将被封装到网络上传送的数据包中,所以,它的结构形式需要保证是网络字节形式。我们这里用到的函数是htons()和htonl(),这些缩写的意思是:h:host,主机(本机)n:network,网络

to:to转换s:short,16位(2字节,常用于端口号)l:long,32位(4字节,常用于IP地址)“反过来”的函数也是存在的ntohs()和ntohl()。动作与持续行为本节最后的一个概念可以跟计算机无关。作为动词,有些可以描述动作,有些是描述一重持续的行为状态的(就如同一般动词和be动词一样)。扯到C++来说,我们可以把持续行为封装到函数内部,只留出动作的接口。事实上,构造函数中的bind()和listen()就是这种描述持续状态的行为函数。5、创建监听嵌套字前面一小节,我们已经写出了TcpServer的构造函数。这个函数的实际作用,就是创建了listensocket(监听嵌套字)。这一节,我们来具体分析这个创建的过程。socket和sockaddr的创建是可以相互独立的在函数中,我们首先通过socket()系统调用创建了listenSock,然后通过为结构体赋值的方法具体定义了服务器端的sockaddr。(memset()函数的作用是把某个内存段的空间设定为某值,这里是清零。)其他的概念已经在前一小节讲完了。这里需要补充的是说明宏定义INADDR_ANY。这里的意思是使用本机所有可用的IP地址。当然,如果你机器绑定了多个IP地址,你也可以指定使用哪一个。数据流简易模型(SOCK_STREAM)我们的例子以电话做的比喻,实际上,socketstream模型不完全类似电话,它至少有以下这些特点:1、一种持续性的连接。这点跟电话是类似的,也可以想象成流动着液体的水管。一旦断开,这种流动就会中断。2、数据包的发送实际上是非连续的。这个世界上有什么事物是真正的线性连续的?呵呵,扯远了,这貌似一个哲学问题。我们仅仅需要知道的是,一个数据包不可能是无限大的,所以,总是一个小数据包一个小数据包这样的发送的。这一点,又有点像邮包的传递。这些数据包到达与否,到达的先后次序本身是无法保证的,即是说,是IP协议无法保证的。但是stream形式的TCP协议,在IP之上,做了一定到达和到达顺序的保证。3、传送管道实际上是非封闭的。要不干嘛叫“网络”-_-!!!。我们之所以能保证数据包的“定点”传送,完全是依靠每个数据包都自带了目的地址信息。由此可见,虽然socket和sockaddr可以分别创建,并无依赖关系。但是在实际使用的时候,一个socket至少会绑定一个本机的sockaddr,没有自己的“地址信息”,就不能接受到网络上的数据包(至少在TCP协议里面是这样的)。

socket与本机sockaddr的绑定有时候绑定是系统的任务,特别是当你不需要知道自己的IP地址和所使用的端口号的时候。但是,我们现在是建立服务器,你必须告诉客户端你的连接信息:IP和Port。所以,我们需要指明IP和Port,然后进行绑定。intbind(intsocket,structsockaddr*localAddress,unsignedintaddressLength);作为C++的程序员,也许你会觉得这个函数很不友好,它似乎更应该写成:intbind_cpp_style(intsocket,constsockaddr&localAddress);我们需要通过函数原型指明两点:1、我们仅仅使用sockaddr结构的数据,但并不会对原有的数据进行修改;2、我们使用的是完整的结构体,而不仅仅是这个结构体的指针。(很显然光用指针是无法说明结构体大小的)幸运的是,在Linux的实现中,这个函数已经被写为:#include/*GivethesocketFDthelocaladdressADDR(whichisLENbyteslong).__THROW;*/externintbind(int__fd,__CONST_SOCKADDR_ARG__addr,socklen_t__len)看到亲切的const,我们就知道这个指针带入是没有“副作用”的。监听:listen()stream流模型形式上是一种“持续性”的连接,这就是要求信息的流动是“可来可去”的。也就是说,stream流的socket除了绑定本机的sockaddr,还应该拥有对方sockaddr的信息。在listen()中,这“对方的sockaddr”就可以不是某一个特定的sockaddr。实际上,listensocket的目的是准备被动的接受来自“所有”sockaddr的请求。所以,listen()反而就不能指定某个特定的sockaddr。intlisten(intsocket,intqueueLimit);其中第二个参数是等待队列的限制,一般设置在5-20。Linux中实现为:#include/*s0onsuccess,-1forerrors.*/externintlisten(int__fd,int__n)__THROW;

完成了这一步,回到我们的例子,就像是让你小弟在电话机前做好了接电话的准备工作。需要再次强调的是,这些行为仅仅是改变了socket的状态,实际上我想强调的是,为什么这些函数不会造成block(阻塞)的原因。(block的概念以后再解释)6、创建“通讯”嵌套字作者:龙飞这里的“通讯”加上了引号,是因为实际上所有的socket都有通讯的功能,只是在我们的例子中,之前那个socket只负责listen,而这个socket负责接受信息并echo回去。我们现看看这个函数:boolTcpServer::isAccept(){unsignedintclntAddrLen=sizeof(clntAddr);if((communicationSock=accept(listenSock,(sockaddr*)&clntAddr,&clntAddrLen))<0){returnfalse;}else{std::cout<<"Client(IP:"<

这个函数实际上起着构造socket作用的仅仅只有第一个参数(另外还有一个不在这个函数内表现出来的因素,后面会讨论到),后面两个指针都有副作用,在socket创建后,会将客户端sockaddr的数据以及结构体的大小传回。当程序调用accept()的时候,程序有可能就停下来等accept()的结果。这就是我们前一小节说到的block(阻塞)。这如同我们调用std::cin的时候系统会等待输入直到回车一样。accept()是一个有可能引起block的函数。请注意我说的是“有可能”,这是因为accept()的block与否实际上决定与第一个参数socket的属性。这个文件描述符如果是block的,accept()就block,否则就不block。默认情况下,socket的属性是“可读可写”,并且,是阻塞的。所以,我们不修改socket属性的时候,accept()是阻塞的。accept()的另一面connect()accept()只是在server端被动的等待,它所响应的,是client端connect()函数:intconnect(intsocket,structsockaddr*foreignAddress,unsignedintaddressLength);虽然我们这里不打算详细说明这个client端的函数,但是我们可以看出来,这个函数与之前我们介绍的bind()有几分相似,特别在Linux的实现中:/*OpenaconnectiononsocketFDtopeeratADDR(whichLENbyteslong).Forconnectionlesssockettypes,justsetthedefaul0onsuccess,-nctionisacancellationpointandthereforenotmarkedwith__THROW.*/externintconnect(int__fd,__CONST_SOCKADDR_ARG__addr,socklen_t__len);connect()也使用了const的sockaddr,只不过是远程电脑上的而非bind()的本机。accept()在server端表面上是通过listensocket创建了新的socket,实际上,这种行为是在接受对方客户机程序中connect()函数的请求后发生的。综合起看,被创建的新socket实际上包含了listensocket的信息以及客户端connect()请求中所包含的信息——客户端的sockaddr地址。新socket与sockaddr的关系accept()创建的新socket(我们例子中的communicationSock,这里我们简单用newSock来带指)首先包含了listensocket的信息,所以,newSock具有本机sockaddr的信息;其次,因为它响应于client端connect()函数的请求,所以,它还包含了clinet端sockaddr的信息。我们说过,stream流形式的TCP协议实际上是建立起一个“可来可去”的通道。用于listen的通道,远程机的目标地址是不确定的;但是newSock却是有指定的本机地址和远程机地址,所以,这个socket,才是我们真正用于TCP“通讯”的socket。inet_ntoa()

#include/*char*inet_ntoa(structin_addr__in)__THROW;*/Thereturnvalue对于这个函数,我们可以作为一种,将IP地址,由in_addr结构转换为可读的ASCII形式的固定用法。7、接收与发送现在,我们通过accept()创建了新的socket,也就是我们类中的数据成员communicationSock,现在,我们就可以通过这个socket进行通讯了。TCP通讯模型在介绍函数之前,我们应该了解一些事实。TCP的Server/Client模型类似这样:ServApp——ServSock——Internet——ClntSock——ClntApp当然,我们这里的socket指的就是用于“通讯”的socket。TCP的server端至少有两个socket,一个用于监听,一个用于通讯;TCP的server端可以只有一个socket,这个socket同时“插”在server的两个socket上。当然,插上listensocket的目的只是为了创建communicationsocket,创建完备后,listen是可以关闭的。但是,如果这样,其他的client就无法再连接上server了。我们这个模型,是client的socket插在server的communicationsocket上的示意。这两个socket,都拥有完整的本地地址信息以及远程计算机地址信息,所以,这两个socket以及之间的网络实际上形成了一条形式上“封闭”的管道。数据包只要从一端进来,就能知道出去的目的地,反之亦然。这正是TCP协议,数据流形式抽象化以及实现。因为不再需要指明“出处”和“去向”,对这样的socket(实际上是S/C上的socket对)的操作,就如同对本地文件描述符的操作一样。但是,尽管我们可以使用read()和write(),但是,为了完美的控制,我们最好使用recv()和send()。recv()和send()intsend(intsocket,constvoid*msg,unsignedintmsgLength,intflags);intrecv(intsocket,void*rcvBuffer,unsignedintbufferLength,intflags);在Linux中的实现为:#include/*nctionisacancellationpointandthereforenotmarkedwith__THROW.*/

externssize_tsend(int__fd,__constvoid*__buf,size_t__n,int__flags);/*nctionisacancellationpointandthereforenotmarkedwith__THROW.*/externssize_trecv(int__fd,void*__buf,size_t__n,int__flags);这两个函数的第一个参数是用于“通讯”的socket,第二个参数是发送或者接收数据的起始点指针,第三个参数是数据长度,第四个参数是控制符号(默认属性设置为0就可以了)。失败时候传回-1,否则传回实际发送或者接收数据的大小,返回0往往意味着连接断开了。处理echo行为voidTcpServer::handleEcho(){constintBUFFERSIZE=32;charbuffer[BUFFERSIZE];intrecvMsgSize;boolgoon=true;while(goon==true){if((recvMsgSize=recv(communicationSock,buffer,BUFFERSIZE,0))<0){throw"recv()failed";}elseif(recvMsgSize==0){goon=false;}else{if(send(communicationSock,buffer,recvMsgSize,0)!=recvMsgSize){throw"send()failed";}}}close(communicationSock);}本小节最后要讲的函数是close(),它包含在中#include/*nctionisacancellationpointandthereforenotmarkedwith__THROW.*/

externintclose(int__fd);这个函数用于关闭一个文件描述符,自然,也就可以用于关闭socket。下一小节是完整的源代码。默认的监听端口是5000。我们可以通过$telnet127.0.0.15000验证在本机运行的echoserver程序。8、本章的完整源代码//Filename:#ifndefTCPSERVERCLASS_HPP_INCLUDED#defineTCPSERVERCLASS_HPP_INCLUDED#include#include#include#includeclassTcpServer{private:intlistenSock;intcommunicationSock;sockaddr_inservAddr;sockaddr_inclntAddr;public:TcpServer(intlisten_port);boolisAccept();voidhandleEcho();};#endif//TCPSERVERCLASS_HPP_INCLUDED//Filename:#include""TcpServer::TcpServer(intlisten_port){if((listenSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0){

throw"socket()failed";}memset(&servAddr,0,sizeof(servAddr));_family=AF_INET;_addr.s_addr=htonl(INADDR_ANY);_port=htons(listen_port);if(bind(listenSock,(sockaddr*)&servAddr,sizeof(servAddr))<0){throw"bind()failed";}if(listen(listenSock,10)<0){throw"listen()failed";}}boolTcpServer::isAccept(){unsignedintclntAddrLen=sizeof(clntAddr);if((communicationSock=accept(listenSock,(sockaddr*)&clntAddr,&clntAddrLen))<0){returnfalse;}else{std::cout<<"Client(IP:"<

throw"send()failed";}}}close(communicationSock);}演示程序://Filename://TcpServerC++style,singlework#include#include""intecho_server(intargc,char*argv[]);intmain(intargc,char*argv[]){intmainRtn=0;try{mainRtn=echo_server(argc,argv);}catch(constchar*s){perror(s);exit(EXIT_FAILURE);}returnmainRtn;}intecho_server(intargc,char*argv[]){intport;if(argc==2){port=atoi(argv[1]);}else{port=5000;}TcpServermyServ(port);while(true){

if(pt()==true){Echo();}}return0;}win32下使用socketWinSock学习socket最好能有两台以上联网的电脑,以及能获得公网IP的网络接入方式。两年前,我主要使用的是一台win2k3和DebainLinux双系统的电脑,例外有台99年的老机器装着win98,而且没有装VC,测试相当的麻烦。现在买了笔记本,使用的是Vista的win32环境(32位),可以直接和老电脑的Linux联网进行测试。另外,网络环境也换成了电信的ADSL,贵了很多,为的就是能有一个公网IP。接下来的教程我会兼顾winsock的代码,这主要是因为winsock本身对socket几乎是兼容的。所以,这里有必要先说明在VC环境中使用socket的一些简单设置,以及与Linux环境下的细微差别。我的VC环境依然是2008Express,在写这篇教程的时候,微软已经发布了VC2010,目前在微软的官方主页,提供了VC2010的下载,同时保留着VC2008的下载。我们在VC中建立一个控制台的空项目:

我们着手构建自己的第一个winsock程序。首先win32下与Linux下的socketAPI需要包含不同的头文件。在Linux下是这些:#include#include#includewin32下的winsock有多个版本,我所找到的资料中,老的版本是:#include与之对应的需要的链接库为:

这可能可以兼容非常古老的版本中的winsock,比如win98,而微软官方所推荐的是:#include链接库是:ws2_,这样就可以使用高版本的winsock。那么,什么是winsock的版本?这就涉及到winsock的初始化函数WSAStartup:/en-us/library/ms742213(v=VS.85).aspx上面是微软的官方说明,我这里构建一个简单的类,希望每次使用的时候引入一个类对象就可以了。classWinsockAPI{private:WSADATAwsaData;public:WinsockAPI(intlow_byte=2,inthigh_byte=2);~WinsockAPI();voidshowVersion()const;};WSADATA是记录着winsock信息的结构。//classWinsockAPIWinsockAPI::WinsockAPI(intlow_byte,inthigh_byte){constWORDwVersionRequested=MAKEWORD(low_byte,high_byte);intwsa_startup_err=WSAStartup(wVersionRequested,&wsaData);if(wsa_startup_err!=0){std::cerr<<"WSAStartup()failed."<

<#includenamespacesockClass{voiderror_info(constchar*s);}classWinsockAPI{private:WSADATAwsaData;public:WinsockAPI(intlow_byte=2,inthigh_byte=2);~WinsockAPI();

voidshowVersion()const;};classBaseSock{protected:intsockFD;public:BaseSock();virtual~BaseSock()=0;constint&showSockFD()const;};classTCPListenSock:publicBaseSock{private:sockaddr_inlistenSockAddr;public:TCPListenSock(unsignedshortlisten_port);~TCPListenSock();voidTCPListen(intmax_connection_requests=10)const;};classTCPServerSock:publicBaseSock{private:sockaddr_inclientSockAddr;protected:char*preBuffer;intpreBufferSize;mutableintpreReceivedLength;public:TCPServerSock(constTCPListenSock&listen_sock,intpre_buffer_size=32);virtual~TCPServerSock();intTCPReceive()const;intTCPSend(constchar*send_data,constint&data_length)const;};#endif////Filename:

#include""//sockClassnamespacesockClass{voiderror_info(constchar*s){std::cerr<

sockFD(-1){}BaseSock::~BaseSock(){}constint&BaseSock::showSockFD()const{returnsockFD;}//classTCPListenSockTCPListenSock::TCPListenSock(unsignedshortlisten_port){sockFD=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);if(sockFD<0){sockClass::error_info("socket()failed.");}memset(&listenSockAddr,0,sizeof(listenSockAddr));_family=AF_INET;_addr.s_addr=htonl(INADDR_ANY);_port=htons(listen_port);if(bind(sockFD,(sockaddr*)&listenSockAddr,sizeof(listenSockAddr))<0){sockClass::error_info("bind()failed.");}}TCPListenSock::~TCPListenSock(){closesocket(sockFD);}voidTCPListenSock::TCPListen(intmax_connection_requests)const{if(listen(sockFD,max_connection_requests)<0){sockClass::error_info("listen()failed.");}

}//classTCPServerSockTCPServerSock::TCPServerSock(constTCPListenSock&listen_sock,intpre_buffer_size):preBufferSize(pre_buffer_size),preReceivedLength(0){preBuffer=newchar[preBufferSize];intclientSockAddrLen=sizeof(clientSockAddr);sockFD=accept(listen_ckFD(),(sockaddr*)&clientSockAddr,&clientSockAddrLen);if(sockFD<0){sockClass::error_info("accept()failed.");}std::cout<<"Client(IP:"<

intTCPServerSock::TCPSend(constchar*send_data,constint&data_length)const{if(data_length>preBufferSize){throw"Dataistoolarge,resizepreBufferSize.";}intsent_length=send(send_data,data_length,0);if(sent_length<0){sockClass::error_info("send()failed.");}elseif(sent_length!=data_length){sockClass::error_info("sentunexpectednumberofbytes.");}returnsent_length;}sockFD,//#ifndefAPP_SOCK_HPP#defineAPP_SOCK_HPP#include""classTCPEchoServer:publicTCPServerSock{public:TCPEchoServer(constTCPListenSock&listen_sock,intpre_buffer_size=32);~TCPEchoServer();boolhandEcho()const;};#endif////Filename:

#include#include""TCPEchoServer::TCPEchoServer(constTCPListenSock&listen_sock,intpre_buffer_size):TCPServerSock(listen_sock,pre_buffer_size){}TCPEchoServer::~TCPEchoServer(){}boolTCPEchoServer::handEcho()const{conststd::stringSHUTDOWN_CMD="/shutdown";while(TCPReceive()>0){std::stringcmd(preBuffer,SHUTDOWN_());if(cmd==SHUTDOWN_CMD&&preReceivedLength==SHUTDOWN_()){returnfalse;}TCPSend(preBuffer,preReceivedLength);}returntrue;}//Filename:#include""#include""intTCP_echo_server(intargc,char*argv[]);intmain(intargc,char*argv[]){intmainRtn=0;try{mainRtn=TCP_echo_server(argc,argv);}catch(constchar*s){perror(s);return1;

}catch(constint&err){std::cerr<<"Error:"<0){listen_port=atoi(argv[1]);}WinsockAPIwinsockInfo;rsion();TCPListenSocklisten_sock(listen_port);listen_ten();boolgo_on=true;while(go_on){TCPEchoServerecho_server(listen_sock);go_on=echo_ho();}return0;}TCP原理1、socket异常信息之所以把对异常信息的介绍放到原理之前讲,是因为由于socket本身的复杂性,导致了产生各种异常的复杂性。我们应该时刻铭记的是,sokcet本身属于系统(OS),是系统对TCP/IP的实现,也就是说,socket发出的异常信息不代表程序出错,甚至不代表系统出错,而仅仅就是代表socket本身的各种异常情况。另外一点我觉得应该强调的是:socket不是TCP/IP;TCP/IP也不是socket。socket是为广泛的协议设计的,涉及TCP/IP的内容只是socket体系

中一个很小的子集;而TCP/IP就更加独立于sokcet而存在——TCP/IP是协议描述;socket是对协议理论的一种实现形式。因为socket是属于系统的,所以不同的系统对于socket有着大同小异的解释,出错描述也不尽相同。在Linux中,socket的异常信息可以通过errno获得(int类型),然后可以通过函数strerror()将int转换成字符串描述;也可以通过函数perror()直接获得其描述。要使用errno需要包含头文件。我建议使用errno获得int类的错误信息的一个重要原因在于,socket的异常不一定就必然导致程序终止。BjarneStroustrup在介绍C++异常机制的时候对C风格的异常机制有着这样的描述:(C++对于异常)的默认响应方式是终止程序。传统的反应(对于发生异常的时候)则是装糊涂,接着做下去,以期得到最好的结果(《C++程序设计语言》第14章异常处理)。不过以我目前的水平看来,终止正在进行的程序然后再通过异常机制重新启动一个新的流程,其代价远远大于“装糊涂”的让程序继续运行下去,只要错误不是致命的,通过简单的判断和处理或许效果更佳。例如,socket中就有一个很有代表性的情况,在TCP连接中,如果一方意外退出——也就是说没有通过TCP退出流程退出,比如没有运行完程序关闭掉socket而直接X掉或者Ctrl+c了。socket往往会因为recv()返回值小于0而抛出一个异常。正常断开连接的时候,recv()会通过返回0表示连接已经断开,但是大多数时候,我们并不希望因为异常的断开就导致另外一端的程序终止(想象一下如果你关掉QQ腾讯的服务器程序就终止是什么概念……),所以我们必须处理这种情况。在Linux中,远程连接异常断开(被重置)的errno代码是104,类似的,我们应该保证出现这种异常的时候程序可以继续运行。//Filename:#ifndefSOCK_CLASS_HPP#defineSOCK_CLASS_HPP#include#include#include#include#includenamespacesockClass{voiderror_info(constchar*s);}以上是头文件中的声明,下面是函数,我们这里仅仅演示处理了104错误。namespacesockClass{voiderror_info(constchar*s){interr_info=errno;std::cerr<

if(err_info==104){return;}exit(1);}}在windows中,错误代码由WSAGetLastError()获得,而无需设置errno。//Filename:#ifndefSOCK_CLASS_HPP#defineSOCK_CLASS_HPP#include#includenamespacesockClass{voiderror_info(constchar*s);}WinSock的错误代码跟Linux中的不一样,同样的异常,WinSock的错误代码是10054。并且,由于没有errno也就无从调用strerror(),我们最好自己写出详细的异常信息。WinSock的详细代码信息在这里:/en-us/library/ms740668(v=VS.85).aspxwin32下的演示代码如下:namespacesockClass{voiderror_info(constchar*s){intwinsock_err=WSAGetLastError();perror(s);std::cerr<<"WinSockError:"<

2、设计TCPsocket的类(上)我们在第1节中讲过,socket是一个int的文件描述符(WinSock中直接是一种抽象的描述符),我们通过对这个描述符发出指令操作socket。这是C语言的思想,在面向对象的思想中,最好socket本身是一种对象,各种方法由对象本身发出。用面向对象的思想封装socket并不困难,而且,对于描述socket的概念可能更加直观,这一节,我们边介绍socket和TCP的概念边对socket进行OO封装。首先,每一个socket对象都具有唯一的socket文件描述符,这样可以很好的对应socket的概念。所以我们构建一个基类,并让其成为纯虚函数——这是因为socket文件描述符必须在具体的构造中才能出现,然后仍然保留一个返回原始的socket文件描述符的接口,这是为了不方便归结到类函数中的函数所预留准备的,比如极其重要的select()我们会在后面讲到,所谓有备无患。classBaseSock{protected:intsockFD;public:BaseSock();virtual~BaseSock()=0;constint&showSockFD()const;};函数实现://classBaseSockBaseSock::BaseSock():sockFD(-1){}BaseSock::~BaseSock(){}constint&BaseSock::showSockFD()const{returnsockFD;}我们把sockFD的初始值设置为-1,表明在没有派生类构造的时候这是一个非法的文件描述符号(FileDescriptor)。接下来,我们简单回顾一下第一节对于TCPServer的建立:首先,我们需要建立一个监听socket,然后激活其监听;然后,在client端连接信息过来之后,通过监听端口将客户端的信息传递给新的socket,从

而建立通讯socket。我们先构建listensocket:classTCPListenSock:publicBaseSock{private:sockaddr_inlistenSockAddr;public:explicitTCPListenSock(unsignedshortlisten_port);~TCPListenSock();voidTCPListen(intmax_connection_requests=10)const;};TCPListenSock建立的目的的就是被动的等待client端寻找握手的connect(),从而收集client端的sock地址信息(包含了IP地址和端口号),然后在需要的时候传递给新的socket建立通讯socket。TCPListenSock::TCPListenSock(unsignedshortlisten_port){sockFD=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);if(sockFD<0){sockClass::error_info("socket()failed.");}memset(&listenSockAddr,0,sizeof(listenSockAddr));_family=AF_INET;_addr.s_addr=htonl(INADDR_ANY);_port=htons(listen_port);if(bind(sockFD,(sockaddr*)&listenSockAddr,sizeof(listenSockAddr))<0){sockClass::error_info("bind()failed.");}}TCPListenSock::~TCPListenSock(){close(sockFD);}TCPListenSock通过调用socket()建立sockFD;通过指定端口好指明监听端口,这是为客户端能够找到这个端口所必须的。而IP地址设置为INADDR_ANY,其实就是0,这意味着可以是任何一个server端所拥有的IP。TCPListenSock通过bind()将sockFD和SockAddr绑定在一起。这个sockFD只有本机的SockAddr意味着:1、无法建立连接,只有接受数据报;

2、只能接受信息,因为没有远程目的地的SockAddr而无法发出信息。而这对于TPC建立连接的过程来说,既是足够的,也是必须的。事实上,client端发出的第一个握手数据报就被这个sockFD所接收,而返回给client的握手应答和对client的握手请求则由新的sockFD发出。listen()是将TCPListenSock激活为监听状态,如果不激活,那么任何握手的连接请求都将被这个sockFD所忽略。voidTCPListenSock::TCPListen(intmax_connection_requests)const{if(listen(sockFD,max_connection_requests)<0){sockClass::error_info("listen()failed.");}}这个函数看来似乎有些多此一举,因为这个监听是可以整合到构造函数中的,也就是说,我们可以一旦建立TCPListenSock就令其激活,事实上这正是SDL_net中的做法,也是让我感到不严谨的地方,因为监听本身是socket的一个概念。3、设计TCPsocket的类(中)当激活监听的TCPListenSock等待远程client的connect()握手请求的时候,是调用了accept()并且产生阻塞(默认情况下),如果accept()成功返回意味着conect()握手请求请求成功,这时候就通过accept()产生了一个新的sockFD用于TCP通讯。我们把这个新的sockFD构建为TCPServerSock类:classTCPServerSock:publicBaseSock{private:sockaddr_inclientSockAddr;protected:char*preBuffer;intpreBufferSize;mutableintpreReceivedLength;public:explicitTCPServerSock(constTCPListenSock&listen_sock,intpre_buffer_size=32);virtual~TCPServerSock();intTCPReceive()const;intTCPSend(constchar*send_data,constint&data_length)const;};

这里,我们为TCPServerSock预留一个缓存,这个缓存并不是必须的,但是设置这样一个缓存至少有两个好处:1、可以在使用时不必专门为recv()建立缓存;2、类方法TCPReceive()和TCPSend()可以共享这个缓存,在处理很多问题时候很方便,比如echo,就不需要先把recv()的缓存读出来再由send()来发送。将缓存已用长度preReceiveLength加上关键字mutable表示我们不关心这个长度会被更改,我们只在乎有一个缓存可以用,但是实际用了多少不重要,这样我们就可以为接受和发送的类方法加上const。我们回到TCPServerSock的建立,TCPServerSock通过TCPListenSockaccept()一个远程的clientconnect()握手请求而建立,所以,TCPServerSock的构造在默认情况下是阻塞的。TCPServerSock::TCPServerSock(constTCPListenSock&listen_sock,intpre_buffer_size):preBufferSize(pre_buffer_size),preReceivedLength(0){preBuffer=newchar[preBufferSize];socklen_tclientSockAddrLen=sizeof(clientSockAddr);sockFD=accept(listen_ckFD(),(sockaddr*)&clientSockAddr,&clientSockAddrLen);if(sockFD<0){sockClass::error_info("accept()failed.");}std::cout<<"Client(IP:"<

sockFD中。请注意,server端的信息并非由TCPListenSock提供,因为TCPListenSock中listenSockAddr的IP地址为空(INADDR_ANY==0),而TCPServerSock中server端的SockAddr却是具体的,由客户端的握手协议传来的(但是没有具体的体现出来)。只有具体的地址(IP地址和端口)才能提供IP数据包的目的地方向。而端口号,则因为client事先知道监听端口号,从而在握手请求中包含,最终传递给TCPListenSock中server端的SockAddr,虽然这个过程决定了这个端口号等于监听端口号,但是需要明白的是,这个端口号来自握手请求的数据报而不是TCPListenSock的listenSockAddr。新的sockFD具有来向(本机)和去向(远程)的信息,所以可以收发数据。TCPServerSock的sockFD一旦建立,马上向远程返回一个数据报,这个数据报有两层意义:1、表示server已经接收了client的握手请求;2、对client发出与server这个新sockFD握手的请求。这就是所谓第二次握手,并且也是以数据报的形式传送的。我们说过,TCP协议的目标是建立“可靠”的数据流形式的通讯,在这个数据流的通道建立起来以前,只能采用数据报的形式传送数据。4、设计TCPsocket的类(下)在另外一边的客户端,我们分析一下TCPClientSock的建立过程。classTCPClientSock:publicBaseSock{private:sockaddr_inserverSockAddr;protected:char*preBuffer;intpreBufferSize;mutableintpreReceivedLength;public:TCPClientSock(constchar*server_IP,unsignedshortserver_port,intpre_buffer_size=32);virtual~TCPClientSock();intTCPReceive()const;intTCPSend(constchar*send_data,constint&data_length)const;};我们看到TCPClientSock的类与TCPServerSock很类似,构造函数的差别是,TCPClientSock需要提供server端的IP地址和端口号。TCPClientSock::TCPClientSock(constchar*server_IP,unsignedshortserver_port,

intpre_buffer_size):preBufferSize(pre_buffer_size),preReceivedLength(0){preBuffer=newchar[preBufferSize];sockFD=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);if(sockFD<0){sockClass::error_info("sock()failed.");}memset(&serverSockAddr,0,sizeof(serverSockAddr));_family=AF_INET;_addr.s_addr=inet_addr(server_IP);_port=htons(server_port);if(connect(sockFD,(structsockaddr*)&serverSockAddr,sizeof(serverSockAddr))<0){sockClass::error_info("connect()failed.");}}TCPClientSock::~TCPClientSock(){delete[]preBuffer;close(sockFD);}TCPClientSock通过socket()建立起sockFD,然后指定服务器的serverSockAddr,然后通过connect()向serverSockAddr指定的服务器发出握手请求。需要说明的是,调用connect()的时候,系统会检查TCPClientSock的sockFD是否已经绑定了本机的SockAddr,事实上我们也可以通过bind()将本机的IP和指定的端口号绑定在这个sockFD上,但是我们并不关心这个IP地址和端口号(况且很多主机并没有公网IP,特别在中国),所以通常我们不自己去绑定,这样系统就会帮我们完成绑定工作,分配一个空闲的端口号作为本机地址的端口号。这样TCPClientSock具有来向(本机地址,通常由系统自动完成绑定,也可以指定)和去向(指定的server端地址)的地址信息,所以可以收发信息。于是,TCPClientSock发出的第一个数据报是发给server监听socket的握手请求数据报,TCPListenSock接收这个数据报后,将相关信息传递给TCPServerSock建立新的sockFD,我们上一节讲到,这个新的sockFD建立起来之后马上就向client端返回一个数据报:一方面表示接受第一次握手请求,另外一方面发出第二次握手请求。收到第二次握手请求后,connect()才会返回,不然就会阻塞,非常“尽力”的去连接server。这个“尽力”的程度跟系统有关,在我的试验中,windows下很快,就几秒;而Debian则接近6分钟!

connect()返回的同时,向server发出了第三次握手的信息,这个信息是对第二次握手请求的认可。所以,第一次和第二次握手包含着连接的请求;而第二次和第三次握手则包含着对握手请求的认可,他们都是在告诉对方:我知道并同意你连接上我了。至此,TCP三次握手的概念在socket中完整的实现,建立起数据流的TCP通信通道。5、TCP的三次握手(three-wayhandshake)前面3个小节介绍了socket机制对TCP协议三次握手的实现,需要强调的是,与协议独立于实现类似,TCP的三次握手是独立于socket体系的理论。在TCP协议中,三次握手是通过3个TCP格式的IP数据报来实现的。TCP格式的IP数据报中包含着TCP首部,TCP首部信息中包含着对每一个数据报具体内容的描述。我们这里需要介绍的首部位(bit)标志只有3个:SYN:同步序号用来发起一个连接。因为TCP协议要求数据传送是可靠的,他的实现方式就是对传输的数据的每一个字节(byte)按顺序编号。但是初始序列号(ISN:InitialSequenceNumber)并非从0开始,而是一个随时间周而复始变化的32位无符号整数。当一方发起连接的时候,SYN就会被设置成1,同时,在发送的数据部分用一个字节来表明这是一个新连接的开始。因此,假设发起连接的一方的ISN为n,因为SYN会在数据部分添加一个字节表示这是一个新连接的开始,所以这时候的字节序号就成了n+1。ACK:确认序号有效。TCP协议要求自动检验数据的可靠性,实现方式就是检验字节序号是否正确的衔接。假如接收数据的一方序号已经是m,那么其返回给发送方确认有效的序号就是m+1。一旦连接,ACK始终设置为1,即表示序号有效,并且在所有数据包中总是存在。但是数据是否真的被TCP采用要看序号是否能对应。如果发送方传来的字节序号没有从m+1开始,那么这个IP数据包就不会被采用,返回ACK信息序号依然是m+1;如果发送方传来的字节序号尽管是从m+1开始的,但是在效验时发生了错误,这个数据报依然不会被采用,返回的ACK信息序号依然是m+1。直到接收了通过TCP检验的数据,序号才会继续增加,例如,传来的数据字节序号从m+1开始到m+k结束,并且通过了TCP效验,那么再次传回的ACK信息,序号就成为了m+k+1。FIN:发送端完成发送。与SYN类似,FIN也会在数据部分占用一个字节,表示这是一个结束符号。TCP的三次握手过程如下:1、第一个SYN连接请求由客户端发起,这个数据报将SYN设置为1表示是一个连接请求,并且包含着这次连接的ISN,我们假设其值为n。2、服务器端收到第一次握手请求的数据报后开始构建反馈的数据报。反馈数据报包括两个部分:第一部分是将连接请求的序号反馈回去,因为SYN本身占了一个字节,所以反馈回去的序号就是n+1;第二部分是自己也向客户端发起SYN连接请求,也将SYN设置为1,并包含这个新连接的ISN,我们设其值为m。3、客户端回应服务器端的SYN连接请求,将服务器端到客户端连接的序号反馈回去,因为SYN占了一个字节,所以反馈给服务器端的序号是m+1。由此,我们可以看到,TCP中,客户端到服务器端,服务器端到客户端的连接是分别建立的,具有不同的ISN(n和m),我们在后面可以看到,这也就意味着这两个连接在正常情况下需要分别的断开。

6、字节流的发送与接收从TCP三次握手的原理我们可以看到,TCP有“保障”的连接实际上可以看做是两个单向的连接:一个通道只负责发送,另外一个只负责接收。并且,传送的信息是以字节为单位保证顺序的。在socket机制中,应用层的程序以send()函数将数据首先发送到本机系统的发送缓存中,我们称之为SendQ,意指这是一个FIFO(先进先出)的队列。这个缓存是系统决定的,并不是在我们的程序中指定的。然后socket机制负责将SendQ中的数据以字节为单位,按照顺序发送给对方的接收缓存RecvQ中。RecvQ也是一个属于系统的FIFO缓存队列。从程序员的角度看,send()函数只负责把数据送入SendQ,而SendQ何时将数据发送则是不可控的。所以,send()通常不会阻塞,只有在不能立即将数据发送给SendQ的时候才会阻塞,这往往是因为SendQ缓存已满。另外,SendQ并不负责统计每次send()所发送来的字节流的长度,事实上这个长度在TCP中没有意义,因为所有数据都以字节为单位按照FIFO的形式排列在队列中,而并不在乎来自于哪一次的send()。这也就是所谓的TCP无边缘保证,TCP的send()并不在乎每次传送的数据有多少,而只是致力于将数据以字节为单位按照FIFO的形式排列在SendQ队列中。我们看一下TCPServerSock和TCPClientSock的TCPSend()方法:intTCPServerSock::TCPSend(constchar*send_data,constint&data_length)const{if(data_length>preBufferSize){sockClass::error_info("Dataistoolarge,resizepreBufferSize.");}intsent_length=send(send_data,data_length,0);if(sent_length<0){sockClass::error_info("send()failed.");}elseif(sent_length!=data_length){sockClass::error_info("sentunexpectednumberofbytes.");}returnsent_length;}intTCPClientSock::TCPSend(constchar*send_data,constint&data_length)const{if(data_length>preBufferSize){sockClass::error_info("Dataistoolarge,resizepreBufferSize.");}sockFD,

intsent_length=send(send_data,data_length,0);if(sent_length<0){sockFD,sockClass::error_info("send()failed.");}elseif(sent_length!=data_length){sockClass::error_info("sentunexpectednumberofbytes.");}returnsent_length;}可以看到,这两个方法除了分属于不同的类名字不一样,其他都是一样的。send()的返回值是实际发送的字节长度。在收信息的另外一边,当RecvQ没有数据时,recv()就会阻塞(默认情况下),每当有数据可接收,recv()就会返回实际接收到的数据长度。recv()同样不在乎每次接收的数据有多少,其参数只有一个最大长度限制,这个限制是应用程序分配给每次recv()储存数据的缓存大小。所以TCP的send()和recv()不是一一对应的:send()只负责将数据写入本机的SendQ,而recv()只负责把本机RecvQ中的数据读出来。假设send()传送了m+n字节,但是第一次到达远程目的地的RecvQ中只有m字节,于是这里的recv()就会马上返回m字节;剩下的n字节第二次才姗姗来迟,那么就需要第二次调用recv()来接收。intTCPServerSock::TCPReceive()const{preReceivedLength=recv(preBuffer,preBufferSize,0);if(preReceivedLength<0){sockClass::error_info("recv()failed.");}elseif(preReceivedLength==0){std::cout<<"Clienthasbeendisconnected.n";return0;}returnpreReceivedLength;}intTCPClientSock::TCPReceive()const{preReceivedLength=recv(preBuffer,preBufferSize,0);if(preReceivedLength<0){sockFD,sockFD,

sockClass::error_info("recv()failed.");}elseif(preReceivedLength==0){std::cout<<"Disconnectedfromserver.n";return0;}returnpreReceivedLength;}可以看到这2个方法也几乎是一模一样——除了名字和对异常信息的描述。因为我们这里并不知道需要recv()的确切长度,所以这里的TCPReceive()也跟recv()一样,有数据就返回。需要验证数据长度的,比如echo服务,我们另外写验证长度的代码。最后需要说明的是,虽然SYN和FIN都会占用一个字节的数据,但是对于应用层的send()和recv()来说是不可见的。FIN会让recv()返回0,表示连接正常断开。7、TCP连接的关闭TCP连接一旦建立,服务器端和客户端就成为了对等关系,任何一方都可以发出关闭握手请求,甚至可以同时发出关闭握手请求。TCP的连接建立需要3次握手,而正常关闭则需要4次握手。1、主动关闭的一方A调用close(),SendQ不再接收send()写入信息,在SendQ队列的最后,向被动关闭的一方发送TCP的IP数据报作为关闭握手的请求。这个数据报中包含着标志FIN,也包含着此刻的字节序号m。2、B接收到第一次关闭握手请求后马上返回一个数据报作为回应。因为B接收到了FIN作为关闭连接的一个字节的数据,所以返回的字节序号是m+1。当A接收到B的这个回应,也即是第二次握手以后,表明确认在A到B的方向上不再有数据传送,A即转入所谓半关闭状态,等待B的关闭请求。而B收到FIN会导致recv()返回零,让应用层知道A到B的连接已经断开。3、B方通知了应用层后也就进入等待关闭的状态。当B开始进入关闭流程,也会由B向A发送一个FIN,同时包含着B到A通讯方向上此刻的字节序号n。4、A接收到B的这个FIN之后,也会将序号n+1反馈给B,自此,表明B到A的方向上不再有数据传送,TCP连接正式成功关闭。以上只是对TCP连接关闭的简单描述,事实上,除了使用close()关闭,还可以使用shutdown(),这样在“半关闭”状态下还可以对TCP做其他的利用,具体内容就请大家自己查阅相关资料了。最后,送上本人对于TCP连接的理解——“双向的单行道”——分别建立连接,也分别断开连接。

TCP应用1、构建echo服务器现在,我们用前面所构建的socket类,重新设计第一章中echo的服务器,然后设计客户端程序。echo服务器的工作原理很简单:1、接收客户端传来的信息;2、将接收到的信息原封不动的返回给客户端。可以看到我们所设计的TCPServerSock类具备了echo服务的所有数据成员,我们只需要添加一个具体的echo方法。因此,我们让设计的echo类从TCPServerSock类中派生出来。//#ifndefAPP_SOCK_HPP#defineAPP_SOCK_HPP#include""classTCPEchoServer:publicTCPServerSock{public:TCPEchoServer(constTCPListenSock&listen_sock,intpre_buffer_size=32);~TCPEchoServer();boolhandEcho()const;};#endif//

将handEcho()设计成返回值为bool是出于以下考虑:因为服务器端通常是无限循环提供服务的,我们希望客户端能简单的对服务器端的控制,比如说进行关闭,这样就不用每次用Ctrl+c来关闭服务器端的程序。所以,handlEcho()返回true表示客户端正常断开,false表示服务器被要求终止。//Filename:#include#include""TCPEchoServer::TCPEchoServer(constTCPListenSock&listen_sock,intpre_buffer_size):TCPServerSock(listen_sock,pre_buffer_size){}TCPEchoServer::~TCPEchoServer(){}boolTCPEchoServer::handEcho()const{conststd::stringSHUTDOWN_CMD="/shutdown";while(TCPReceive()>0){std::stringcmd(preBuffer,SHUTDOWN_());if(cmd==SHUTDOWN_CMD&&preReceivedLength==(int)SHUTDOWN_()){returnfalse;}TCPSend(preBuffer,preReceivedLength);}returntrue;}我们为服务器指定一个关闭的的特殊字符串/shutdown,如果客户端传来这个字符串,服务器就会终止;其他字符串则会履行echo服务。最后我们设计主程序://Filename:#include""#include""intmain(intargc,char*argv[]){constunsignedshortDEFAULT_PORT=5000;unsignedshortlisten_port=DEFAULT_PORT;

if(argc==2&&atoi(argv[1])>0){listen_port=atoi(argv[1]);}TCPListenSocklisten_sock(listen_port);listen_ten();boolgo_on=true;while(go_on){TCPEchoServerecho_server(listen_sock);go_on=echo_ho();}return0;}主程序以第一个参数(argv[1])来指定服务器端口,如果不指定,则默认端口是5000。本节源代码下载:Linux:/files/n32:/files/2、构建echo客户端echo客户端的工作原理也很简单:1、向服务器端发送一个字符串;2、接收服务器的返回信息(如果是echo服务器就会返回发送出去的字符串本身)。3、在标准输出中回显服务器返回的信息。与ehco服务器类似,我们的echo客户端类也可以从TCPClientSock中派生出来://#ifndefAPP_SOCK_HPP#defineAPP_SOCK_HPP#include#include""classTCPEchoClient:publicTCPClientSock{public:TCPEchoClient(

constchar*server_IP,unsignedshortserver_port,intpre_buffer_size=32);~TCPEchoClient();booldoEcho(conststd::string&echo_message)const;};#endif//我们的doEcho()接收一个C++风格的字符串(std::string),将返回值设计成bool是出于以下考虑:我们希望与服务器断开连接的信息能反馈到主程序中,并且在断开连接后终止echo客户端的程序。所以,返回true表示仍然与服务器保持连接,否则则已经断开(或者异常)。#include""TCPEchoClient::TCPEchoClient(constchar*server_IP,unsignedshortserver_port,intpre_buffer_size):TCPClientSock(server_IP,server_port,pre_buffer_size){}TCPEchoClient::~TCPEchoClient(){}boolTCPEchoClient::doEcho(conststd::string&echo_message)const{if(TCPSend(echo_(),echo_())<0){returnfalse;}size_ttotal_received_length=0;while(total_received_length

度一样。虽然事实上在这种小数据的传输中很难遇到以上所描述的那种情况,但是在网络程序的设计中,应该坚持这样一个基本假设:你永远不知道远程的主机会出什么状况,所以永远以最坏的可能性来设计程序。最后是主程序。主程序在标准输入中阻塞等待用于echo的信息,为了避免无限循环,我们也设计一个可以关闭服务器端的命令/exit。这样,输入/exit或者服务器断开都可以导致客户端终止。#include""#include""intmain(intargc,char*argv[]){unsignedshortserver_port=5000;if(argc==3&&atoi(argv[2])>0){server_port=atoi(argv[2]);}WinsockAPIwinsockInfo;rsion();TCPEchoClientecho_client(argv[1],server_port);std::stringmsg;boolgo_on=true;while(msg!="/exit"&&go_on==true){std::cout<<"Echo:";std::getline(std::cin,msg);go_on=echo_(msg);}return0;}本节源代码下载:linux:/files/n32:/files/


本文标签: 数据 信息 需要 连接 地址