自从我进去游戏设计的工作以后感觉身体不行,我的睡眠就很烂改怎么做,改怎么弥补睡眠,怎么才能保住睡眠质量

扫一扫,访问微社区
后使用快捷导航没有帐号?
签到成功!您今天第{todayrank}个签到,签到排名竞争激烈,记得每天都来签到哦!已连续签到:{constant}天,累计签到:{days}天
关注:1896
当前位置: &
__________________________________________________________________________________
开发者干货区版块规则:
  1、文章必须是图文形式。(至少2幅图)
& && &2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
& && &3、本版块只支持在游戏蛮牛原创首发,不支持转载。
& && &4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
& && &5、......
__________________________________________________________________________________
查看: 2186|回复: 4
【游戏设计模式】之一 序言:架构,性能与游戏
9排名<font color="#FF昨日变化4主题帖子积分
蛮牛币1601
在线时间74 小时
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
才可以下载或查看,没有帐号?
一、系列文章前言
承接《代码整洁之道》的精读与演绎,《Game Programming Patterns》是我们下一个目标。
写这个系列的起因,是因为最近闲暇时一直在阅读一些之前一直想看的经典著作,并有将阅读过程中一些思考和总结记录下来。为了不枉费这些阅读、思考与总结的过程,决定将这些零散的内容整理成文,沉淀下来。过些年后再回首,也许会觉得当时的一些思考,弥足珍贵。
这个系列的文章,不仅仅是读书笔记,不仅仅是翻译,而是对一本书核心内容的全新演绎,内容解刨,精炼与重组。希望自己的这些文章,能对各位想进一步了解这些经典著作的读者们有所帮助。
二、《Game Programming Patterns》其书
《Game ProgrammingPatterns》,正如其名,是一本专注于游戏编程领域的设计模式指南,它涵盖了游戏逻辑,游戏编辑器,和的编程中的常用技法。作者Robert Nystrom有二十年的从业经验,在EA工作8年。
“这本书将游戏开发中经常涉及到的编程模式拎出来,结合具体开发中遇到的实例一步步的引出对应的模式,这比起四人帮的《设计模式》更加具体。”
1.png (477.35 KB, 下载次数: 5)
10:46 上传
不同于传统的出版方式,这本书是网络出版,然后Web版完全免费,其更是在amazon上具有罕见的5星评价,可见读者对其的好评程度之高。加之书中内容生动有趣,将各种经验之谈娓娓道来,实在是业界良心。
书本主页:/
Web版全文阅读:/contents.html
三、本文涉及知识点思维导图
先放出这篇文章所涉及内容知识点的一张思维导图,就开始正文。大家若是疲于阅读文章正文,直接看这张图,也是可以Get到本文的主要知识点的大概。
2.png (165.4 KB, 下载次数: 4)
10:47 上传
四、何为好的软件架构
《Game ProgrammingPatterns》一书中说,好的设计意味着当我改了点什么, 整个程序就好像正在等着这种改动。我们可以加入几个函数调用完成任务,同时丝毫不改变代码平静表面下的脉动。
这听很酷,只是实行起来很难。“把代码写到改变不会影响其平静表面。”若真能做到,确实不错。
这样太理想化了,还是让我们通俗些吧。架构是有关于变化的,让我们拥抱变化,从变化开始入手。总有人改动代码。如果没人碰代码,无论是因为代码至善至美,还是糟糕透顶,那么它的架构设计就毫无意义。评价架构设计就是评价它应对变化有多么轻松。没有了变化,它就是永远不会离开起跑线的运动员。
应对变化很轻松,这就是好的软件架构的主要优点之一。
五、一个新特性的实现过程
在你改变代码去添加新特性,去修复漏洞,或者随便什么需要使用编辑器的时候, 你需要理解现在的代码在做些什么。当然,你不需要理解整个程序,但你需要将所有相关的东西装进你的灵长类大脑。
我们通常无视了这步,但这往往是编程中最耗时的部分。 如果你认为将数据从磁盘上分页到RAM上很慢, 那么试着通过一对神经纤维将数据分页到大脑中。
一旦把所有正确的上下文都记到了你的大脑里, 想一会,然后找到解决方案。 这可能会有来回打转的时刻,但通常比较简单。一旦你理解了和需要改动的代码,实际的编码工作就很容易了。
你将一些代码加入了游戏,但不想下一个人被你留下来的小问题绊倒。 除非改动很小,否则就还需要一些工作去微调新代码,使之无缝对接到程序的其他部分。如果做对了,那么下个见到代码的人甚至无法说出哪些代码是新加入的。
简而言之,编程的流程图看起来是这样的:
3.png (27.68 KB, 下载次数: 8)
10:48 上传
PS:看起来,这是一个令不少程序员听之色变的死循环:)
六、解耦与学习阶段
其实,很多软件架构都和学习阶段(learning phase)息息相关。 将代码载入到神经元太过缓慢,找些策略减少载入的总量是很值得做的事。GPP(以后不妨将《Game ProgrammingPatterns》一书简称为GPP)一书中有整整一章是关于解耦模式(decoupling patterns), 还有很多常规的设计模式也牵扯到了解耦。
可以用多种方式定义“解耦”,这边是其中之一的理解方式:
如果有两块代码是耦合的, 那就意味着无法仅仅只理解了其中一个,而对另一个丝毫不了解。如果解耦了他俩,就可以独自的理解其中之一,根本无需牵扯到另一个。
GPP一书中说道,我所理解的软件架构的关键目标,就是最小化在处理前需要进入大脑的知识。
当然,也可以从后期阶段来看。 那么,另一种解耦的定义是:当一块代码有变化时,没必要修改另外的代码。 肯定需要修改一些东西,但耦合程度越小,变化会波及的范围就越小。
七、过度设计的代价
首先,来一个设想。
只要解耦掉任何内容,然后,风烟俱净,天山共色,从流飘荡,任意东西,就可以像风一样写代码。每个变化都只修改一两个特定方法,万花丛中过,片叶不沾身,这是多么的惬意,是吧?
这大概就是人们对抽象,模块化,设计模式和软件架构兴奋的原因。在有好架构的程序上工作是很好的体验,每个人都希望能更有效率地工作。好架构能造成生产力上巨大的不同。很难再夸大它那强力的影响。
但是,就像生活中的任何事物一样,没有免费的午餐。好的设计需要汗水和纪律。 每次做出改动或是实现特性,你都需要将它优雅的集成到程序的其他部分。需要花费大量的努力去管理代码, 在开发过程中面对数千次变化仍然保持它的管理结构。
我们会看到无数程序有个优雅的开始,然后死于程序员一遍又一遍添加的“微小黑魔法”。就像园艺,仅仅增加新植物是不够的,还需要除草和修剪。
你得考虑程序的哪部分需要解耦,然后再引入抽象。同样,你需要决定哪部分要设计得支持插件来方便未来的变化。(所谓的面向未来编程)。
人们对这点变得狂热。他们设想以后的开发者(或者只是未来的他们自己)进入代码库,并发现它极为开放,功能强大,极具扩展性,他们发行“有此游戏引擎,夫复何求”。
当过分关注这点时,你会得到失控的代码库。 接口和抽象无处不在。插件系统,抽象基类,虚方法,还有各种各样的扩展点。
当需求变更时,有可能某个接口能帮上忙,但能不能找到就只能祝你好运了。 理论上,解耦意味着在修改代码之前需要了解的代码更少,但其实你需要对抽象层有很多的了解。
但是,还是那句话,理想很丰满,现实很骨感。 每当你添加了一层抽象或者支持扩展的部分,其实就是在赌这部分功能以后是否用得上。 添加代码和复杂性到游戏中,这都需要时间来开发,调试和维护。如果你猜对了,后来使用了这些代码,那么功夫不负有心人。 但预测未来很难,如果模块化最终无益,那就有害。 毕竟,你得花时间去实现这些代码。有些人喜欢简写为术语“YAGNI”——You aren't gonna need it(你不需要那个)——来对抗预测将来需求的强烈欲望。
过度去关注设计模式和软件架构,会让一批人很容易地沉浸在代码中,而忽略要自己的最终目的是要发布游戏。无数的开发者听着加强可扩展性的“警世名言”,花费多年时间制作“引擎”, 却没有搞清楚做引擎是为了什么。
八、性能与速度
软件架构和抽象有时会被批评,尤其是在游戏开发中: 它伤害了游戏的性能。 许多让代码更灵活的模式依靠虚拟调度、 接口、 指针、 消息,和其他机制, 而这些都会消耗运行时成本。
一个有趣的反面例子是C++中的模板。模板编程有时可以给你抽象接口而无需运行时开销。
这是灵活性的两极。当写代码调用类中的具体方法时,你在写作时修改类——硬编码了调用的是哪个类。但通过虚方法或接口,直到运行时才知道调用的类。这更加灵活但增加了运行时开销。
模板编程是在两者之间。在编译时初始化模板,决定调用哪些类。
还有一个原因。很多软件架构的目的是使程序更加灵活。 这让改变它需要较少的努力。编码时对程序有更少的假设。你可以使用接口,让代码可与任何实现它的类交互,而不仅仅是现在写的类。灵活性可以让我们快速改进游戏。
让你的程序更加灵活,在损失一点点性能的前提下更快地做出原型。 但需要注意,优化现有的代码可能会让代码丧失原有的灵活性。
而一种折中的办法是保持代码灵活直到设计定下来,再抽出抽象层来提高性能。
九、烂代码在原型阶段的优势
野百合也有春天,之前在《代码整洁之道》中被我们吐槽的烂代码,其实也有它们的优势——快。
我们知道,编写良好架构的代码需要仔细地思考,这会转为时间上的代价。 在项目的整个周期中保持良好的架构需要花费大量的努力。 你需要像露营者处理营地一样小心处理代码库:总是保持其优于你刚刚接触它的时候。就像我们之前《代码整洁之道》系列文章第一篇中说到的:让代码比你来时更干净。
当你要在项目上花费很久时间的话,保持编写良好架构的代码的习惯,是非常值得推崇的。但你知道,游戏开发需要很多实验、探索与试错。 特别是在早期,写一些你知道要扔掉的代码是很普遍的事情。
而如果只想试试游戏的某些主意是不是正确的, 良好的设计意味着在屏幕上看到和获取反馈之前要消耗很长时间。如果最后证明这点子不对,那么删除代码时,你花费的那些为了让代码更加优雅的额外时间,就白费了。
但你得让人们清楚,可抛弃的代码即使看上去能工作,也不能被维护,必须重写。如果有可能要维护这段代码,就得防御性好好编写它。
一个保证原型代码不会变成真正使用的代码的技巧是使用和正式游戏不同的编程语言。这样,在实际应用于正式游戏中之前必须重写。
在原型开发阶段,能尽快让你做出原型产品,最终让产品成功上线的最初的功臣,或许就是设计糟糕的烂代码。因为他们实现想法够快,不需要缜密的设计与架构。只是这些烂代码在经历了原型设计阶段之后,一定要被重写或者重构。
十、开发周期中因素的动态平衡
在整个开发周期中,如下三大要素一直在相互角力:
为了在项目的整个生命周期保持其可读性,我们需要好架构。
需要更好的运行时性能。
需要让现在的特性更快的实现。
有趣的是,这三点都是速度:长期开发的速度,游戏运行的速度,和短期开发的速度。
这些目标至少是部分对立的。 好架构长期来看提高了生产力, 也意味着维护每个变化都需要更多努力让代码保持整洁。
实现起来最快的代码很少是运行时最快的。 相反,提升性能需要很多的编程时间。而且一旦完成,它就会污染代码库:高度优化的代码不灵活,很难改动。
总有今日事今日毕的压力。但是如果尽可能快地实现特性,代码库就会充满黑魔法,漏洞和混乱,阻碍未来的产出。
对于这个三者的权衡,没有简单明了的解决方案,只有具体问题具体分析,按实际的项目状况去去权衡,让三者保持友好的动态平衡,让整个项目保持良好的状态。
十一、本文涉及知识点提炼整理
本文涉及知识点提炼整理,一些关于游戏架构与性能的心得总结:
1 抽象和解耦会让代码的扩展性和灵活性更加强,但会花费额外的实现时间。除非你觉得这样的灵活性有必要,否则没必要过度的去追求。
2 性能优化很重要,但是要注意时机。在整个开发周期中,最好先专注于实现基本需求,把那些可能限制到项目进度的性能优化尽量延后。
3 在整个开发周期中,灵活性和高性能往往不能兼得。我们可以保持代码的灵活性直到设计定下来,再抽出抽象层来提高性能。
4 在原型开发阶段,能尽快让你做出原型产品,最终让产品成功上线的最初的功臣,或许就是设计糟糕的烂代码。因为他们实现想法够快,不需要缜密的设计与架构。只是这些烂代码在经历了原型设计阶段之后,一定要被重写或者重构。
5 如果打算抛弃这段代码,就不要尝试将其写完美。“摇滚明星将旅店房间弄得一团糟,因为他们知道明天会有人来打扫干净。”
6 提倡去写出最简单,最直接的整洁代码。你读过这种代码后,完全理解了它在做什么,想不出其他完成的方法。“完美是可达到的,不是没有东西可以添加的时候,而是没有东西可以删除的时候。”
7 但最重要的是,如果你想要做出让人享受的东西,那就享受做它的过程。
本文就此结束。
With Best Wishes.
每日推荐:
3161/300主题帖子积分
偶尔光临, 积分 161, 距离下一级还需 139 积分
偶尔光临, 积分 161, 距离下一级还需 139 积分
在线时间95 小时
楼主,后续部分呢?催更
每日推荐:
61121/1500排名<font color="#FF昨日变化1主题帖子积分
蛮牛粉丝, 积分 1121, 距离下一级还需 379 积分
蛮牛粉丝, 积分 1121, 距离下一级还需 379 积分
蛮牛币2787
在线时间407 小时
好文章,求续集呐
每日推荐:
61415/1500排名<font color="#FF昨日变化1主题帖子积分
蛮牛粉丝, 积分 1415, 距离下一级还需 85 积分
蛮牛粉丝, 积分 1415, 距离下一级还需 85 积分
蛮牛币2564
在线时间497 小时
学习了~~~~~~
每日推荐:
141/50排名<font color="#FF昨日变化15主题帖子积分
注册看看, 积分 41, 距离下一级还需 9 积分
注册看看, 积分 41, 距离下一级还需 9 积分
在线时间20 小时
没有帖子收藏的功能吗??
每日推荐:
蛮牛论坛干货区作者
认证开发者
经过游戏蛮牛认证的独立开发者实现http 请求步骤:
1。建立到服务器的TCP连接
2。向服务器发送GET或者POST报文,报文格式请参考HTTP协议
3。接收服务器返回的报文
http://blog.csdn.net/wbczyh/article/details/2117094
平常我们要访问某个URL一般都是通过浏览器进行:提交一个URL请求后,浏览器将请求发向目标服务器或者代理服务器,目标服务器或者代理服务器返回我们所需要的数据,浏览器接收到这些数据后保存成文件并进行显示。
下面我们看看如何自己利用winsock2.h中的接口来实现这个功能?为了简化问题,作以下假设:
通过代理服务器进行HTTP访问,这样就省去了对URL进行DNS解析的步骤,假设代理服务器的地址为:192.168.0.1:808。
这个功能由以下几个部分组成:
1. 如何建立连接?
2. 如何发送请求?
3. 如何接收数据?
4. 如何判断数据接收完成?
下面我们依次来看下这些问题如何解决?
一、如何建立与服务器之间的连接
HTTP基本TCP,所以我们需要与服务器建立连接,然后才能发送数据。
建立连接参考如下函数socket_open:
*打开Socket,返回socketId,-1表示失败
int socket_open(int IP,int Port,int type){
SOCKET socketId;
struct sockaddr_in serv_
socketId=socket(AF_INET,SOCK_STREAM,0);
if((int)socketId&0)
printf(&#8220;[ERROR]Create a socket failed!/n&#8221;);
return -1;
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr = ntohl(IP);
serv_addr.sin_port = htons((USHORT)Port);
status=connect(socketId,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
if(status!=0)
printf(&#8220;[ERROR]Connecting failed!/n&#8221;);
closesocket(socketId);
return -1;
return socketId;
调用方式如下:
int socketId=socket_open(0xC0A); //0xC0A88.0.1的十六进制写法。
二、如何发送请求
发送数据要根据HTTP协议的要求附加协议头:
static const char* protocolHead=&#8221;GET /index.html HTTP/1.1/n&#8221;
&#8220;Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*/n&#8221;
&#8220;Accept-Language: zh-cn/n&#8221;
&#8220;User-Agent:iPanelBrowser/2.0/n&#8221;
&#8220;Host: :80/n&#8221;
&#8220;Connection: close/n/n&#8221;
这里使用GET来获取指定URL的指定文档。
建立连接后使用send将这些数据发送出去:
send(socketId, protocolHead,strlen(protocolHead),0);
发送完成HTTP请求后就等待接收数据。
三、如何接收数据
这里采用select循环查询的方式来判断有无数据到来:
struct timeval tm = {0,7};
fd_set fds_r;
char recvBuf[4096]={‘/0’};
FD_ZERO(&fds_r);
FD_SET(socketId,&fds_r);
status=select(socketId+ 1, &fds_r, 0, 0, &tm); //socketId在这里是最大的fd
if(status & 0 && FD_ISSET(socketId, &fds_r))
printf(&#8220;Socket is readable&#8230;fd=[%d]/n&#8221;,socketId);
recv(socketId,recvBuf,4096,0);
这样数据包就保存到缓冲区中了。
四、如何判断数据接收完成
首先对返回数据的状态进行判断,仅当状态为“ HTTP 200 OK ”时才表明正确返回,这时才对数据进行解析并保存,如果状态为HTTP 404 NOT FOUND或者其它状态则表明没有找到资源或者出现其它问题,可参考。
当数据正确返回时,为了将实际数据从协议中分离出来进行保存,需要对HTTP数据包进行解析得到Content-Length,然后在包含Content-Length的当前数据包或者随后的数据包中查找第一个空行,这就是内容(Content)的开始位置,再配合前面解析得到的Content-Length,就能够知道什么时候数据接收完成了。换行符为“/r/n”,也兼容“/n”或者“/r”,设换行符为^P,则空行如果位于内容中间或结尾则可查找“^P^P”,若位于开头,则查找^P。
基本就是上面这些,这四个问题解决了,那么整个问题也就解决了!
当然也可以把上面的3、4步放一起。
http://blog.csdn.net/gideal_wang/article/details/4316691
一 原理区别
一般在浏览器中输入网址访问资源都是通过GET方式;在FORM提交中,可以通过Method指定提交方式为GET或者POST,默认为GET提交
Http定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET,POST,PUT,DELETE
URL全称是资源描述符,我们可以这样认 为:一个URL地址,它用于描述一个网络上的资源,而HTTP中的GET,POST,PUT,DELETE就对应着对这个资源的查 ,改 ,增 ,删 4个操作。到这里,大家应该有个大概的了解了,GET一般用于获取/查询 资源信息,而POST一般用于更新 资源信息(个人认为这是GET和POST的本质区别,也是协议设计者的本意,其它区别都是具体表现形式的差异 )。
根据HTTP规范,GET用于信息获取,而且应该是安全的和幂等的 。
1.所谓安全的意味着该操作用于获取信息而非修改信息。换句话说,GET请求一般不应产生副作用。就是说,它仅仅是获取资源信息,就像数据库查询一样,不会修改,增加数据,不会影响资源的状态。
* 注意:这里安全的含义仅仅是指是非修改信息。
2.幂等的意味着对同一URL的多个请求应该返回同样的结果。这里我再解释一下幂等 这个概念:
  幂等 (idempotent、idempotence)是一个数学或计算机学概念,常见于抽象代数中。
幂等有以下几种定义:
对于单目运算,如果一个运算对于在范围内的所有的一个数多次进行该运算所得的结果和进行一次该运算所得的结果是一样的,那么我们就称该运算是幂等的。比如绝对值运算就是一个例子,在实数集中,有abs(a) =abs(abs(a)) 。
对于双目运算,则要求当参与运算的两个值是等值的情况下,如果满足运算结果与参与运算的两个值相等,则称该运算幂等,如求两个数的最大值的函数,有在在实数集中幂等,即max(x,x)
看完上述解释后,应该可以理解GET幂等的含义了。
但在实际应用中,以上2条规定并没有这么严格。引用别人文章的例子:比如,新闻站点的头版不断更新。虽然第二次请求会返回不同的一批新闻,该操 作仍然被认为是安全的和幂等的,因为它总是返回当前的新闻。从根本上说,如果目标是当用户打开一个链接时,他可以确信从自身的角度来看没有改变资源即可。
根据HTTP规范,POST表示可能修改变服务器上的资源的请求 。继续引用上面的例子:还是新闻以网站为例,读者对新闻发表自己的评论应该通过POST实现,因为在评论提交后站点的资源已经不同了,或者说资源被修改了。
上面大概说了一下HTTP规范中,GET和POST的一些原理性的问题。但在实际的做的时候,很多人却没有按照HTTP规范去做,导致这个问题的原因有很多,比如说:
1.很多人贪方便,更新资源时用了GET,因为用POST必须要到FORM(表单),这样会麻烦一点。
2.对资源的增,删,改,查操作,其实都可以通过GET/POST完成,不需要用到PUT和DELETE。
3.另外一个是,早期的但是Web MVC框架设计者们并没有有意识地将URL当作抽象的资源来看待和设计 。还有一个较为严重的问题是传统的Web MVC框架基本上都只支持GET和POST两种HTTP方法,而不支持PUT和DELETE方法。
* 简单解释一下MVC:MVC本来是存在于Desktop程序中的,M是指数据模型,V是指用户界面,C则是控制器。使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。
以上3点典型地描述了老一套的风格(没有严格遵守HTTP规范),随着架构的发展,现在出现REST(Representational State Transfer),一套支持HTTP规范的新风格,这里不多说了,可以参考《RESTful Web Services》。
二 表现形式区别
搞清了两者的原理区别,我们再来看一下他们实际应用中的区别:
为了理解两者在传输过程中的不同,我们先看一下HTTP协议的格式:
HTTP请求:
&request line&
&blank line&
&request-body&]
在HTTP请求中,第一行必须是一个请求行(request line),用来说明请求类型、要访问的资源以及使用的HTTP版本。紧接着是一个首部(header)小节,用来说明服务器要使用的附加信息。在首部之后是一个空行,再此之后可以添加任意的其他数据[称之为主体(body)]。
GET与POST方法实例:
GET /books/?sex=man&name=Professional HTTP/1.1
User-Agent: Mozilla/5.0 (W U; Windows NT 5.1; en-US; rv:1.7.6)
Gecko/ Firefox/1.0.1
Connection: Keep-Alive
POST / HTTP/1.1
User-Agent: Mozilla/5.0 (W U; Windows NT 5.1; en-US; rv:1.7.6)
Gecko/ Firefox/1.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 40
Connection: Keep-Alive
(&#8212;-此处空一行&#8212;-)
name=Professional%20Ajax&publisher=Wiley
有了以上对HTTP请求的了解和示例,我们再来看两种提交方式的区别:
(1)GET提交,请求的数据会附在URL之后(就是把数据放置在请求行(request line)中),以?分割URL和传输数据,多个参数用&连接;例如:login.action?name=hyddd&password=idontknow&verify=%E4%BD%A0 %E5%A5%BD。Url的编码格式采用的是ASCII码,而不是Unicode,这也就是说你不能在Url中包含任何非ASCII字符,所有非ASCII字符均需要编码再传输,关于Url编码可参考:。
POST提交:把提交的数据放置在是HTTP包的包体中。上文示例中红色字体标明的就是实际的传输数据
因此,GET提交的数据会在地址栏中显示出来,而POST提交,地址栏不会改变
(2)传输数据的大小:首先声明:HTTP协议没有对传输的数据大小进行限制,HTTP协议规范也没有对URL长度进行限制。
而在实际开发中存在的限制主要有:
GET:特定浏览器和服务器对URL长度有限制,例如IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持。
因此对于GET提交时,传输数据就会受到URL长度的限制。
POST:由于不是通过URL传值,理论上数据不受限。但实际各个WEB服务器会规定对post提交数据大小进行限制,Apache、IIS6都有各自的配置。
(3)安全性:
.POST的安全性要比GET的安全性高。注意:这里所说的安全性和上面GET提到的“安全”不是同个概念。上面“安全”的含义仅仅是不作数据修改,而这 里安全的含义是真正的Security的含义,比如:通过GET提交数据,用户名和密码将明文出现在URL上,因为(1)登录页面有可能被浏览器缓存, (2)其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了,除此之外,使用GET提交数据还可能会造成Cross-site request forgery攻击
(4)Http get,post,soap协议都是在http上运行的
1)get:请求参数是作为一个key/value对的序列(查询字符串)附加到URL上的
查询字符串的长度受到web浏览器和web服务器的限制(如IE最多支持2048个字符),不适合传输大型数据集同时,它很不安全
2)post:请求参数是在http标题的一个不同部分(名为entity body)传输的,这一部分用来传输表单信息,因此必须将Content-type设置为:application/x-www-form-urlencoded。post设计用来支持web窗体上的用户字段,其参数也是作为key/value对传输。
但是:它不支持复杂数据类型,因为post没有定义传输数据结构的语义和规则。
3)soap:是http post的一个专用版本,遵循一种特殊的xml消息格式
Content-type设置为: text/xml
任何数据都可以xml化
三 HTTP响应
1.HTTP响应格式:
&status line&
&blank line&
[&response-body&]
在响应中唯一真正的区别在于第一行中用状态信息代替了请求信息。状态行(status line)通过提供一个状态码来说明所请求的资源情况。
HTTP响应实例:
HTTP/1.1 200 OK
Date: Sat, 31 Dec :59 GMT
Content-Type: text/charset=ISO-8859-1
Content-Length: 122
<title>Wrox Homepage</title>
<!&#8211; body goes here &#8211;>
2.最常用的状态码有:
◆200 (OK): 找到了该资源,并且一切正常。
◆304 (NOT MODIFIED): 该资源在上次请求之后没有任何修改。这通常用于浏览器的缓存机制。
◆401 (UNAUTHORIZED): 客户端无权访问该资源。这通常会使得浏览器要求用户输入用户名和密码,以登录到服务器。
◆403 (FORBIDDEN): 客户端未能获得授权。这通常是在401之后输入了不正确的用户名或密码。
◆404 (NOT FOUND): 在指定的位置不存在所申请的资源。
四 完整示例:
GET /DEMOWebServices2.8/Service.asmx/CancelOrder?UserID=string&PWD=string&OrderConfirmation=string HTTP/1.1
HTTP/1.1 200 OK
Content-Type: text/ charset=utf-8
Content-Length: length
&?xml version=&#&#8243; encoding=&#8221;utf-8&#8243;?&
&objPlaceOrderResponse xmlns=&#8221;/webservices2.3&#8243;&
&Success&boolean&/Success&
&ErrorDescription&string&/ErrorDescription&
&ErrorNumber&int&/ErrorNumber&
&CustomerOrderReference&long&/CustomerOrderReference&
&OrderConfirmation&string&/OrderConfirmation&
&CustomerDealRef&string&/CustomerDealRef&
&/objPlaceOrderResponse&
POST /DEMOWebServices2.8/Service.asmx/CancelOrder HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: length
UserID=string&PWD=string&OrderConfirmation=string
HTTP/1.1 200 OK
Content-Type: text/ charset=utf-8
Content-Length: length
&?xml version=&#&#8243; encoding=&#8221;utf-8&#8243;?&
&objPlaceOrderResponse xmlns=&#8221;/webservices2.3&#8243;&
&Success&boolean&/Success&
&ErrorDescription&string&/ErrorDescription&
&ErrorNumber&int&/ErrorNumber&
&CustomerOrderReference&long&/CustomerOrderReference&
&OrderConfirmation&string&/OrderConfirmation&
&CustomerDealRef&string&/CustomerDealRef&
&/objPlaceOrderResponse&
POST /DEMOWebServices2.8/Service.asmx HTTP/1.1
Content-Type: application/soap+ charset=utf-8
Content-Length: length
&?xml version=&#&#8243; encoding=&#8221;utf-8&#8243;?&
&soap12:Envelope xmlns:xsi=&#8221;http://www.w3.org/2001/XMLSchema-instance&#8221; xmlns:xsd=&#8221;http://www.w3.org/2001/XMLSchema&#8221; xmlns:soap12=&#8221;http://www.w3.org/2003/05/soap-envelope&#8221;&
&soap12:Body&
&CancelOrder xmlns=&#8221;/webservices2.3&#8243;&
&UserID&string&/UserID&
&PWD&string&/PWD&
&OrderConfirmation&string&/OrderConfirmation&
&/CancelOrder&
&/soap12:Body&
&/soap12:Envelope&
HTTP/1.1 200 OK
Content-Type: application/soap+ charset=utf-8
Content-Length: length
&?xml version=&#&#8243; encoding=&#8221;utf-8&#8243;?&
&soap12:Envelope xmlns:xsi=&#8221;http://www.w3.org/2001/XMLSchema-instance&#8221; xmlns:xsd=&#8221;http://www.w3.org/2001/XMLSchema&#8221; xmlns:soap12=&#8221;http://www.w3.org/2003/05/soap-envelope&#8221;&
&soap12:Body&
&CancelOrderResponse xmlns=&#8221;/webservices2.3&#8243;&
&CancelOrderResult&
&Success&boolean&/Success&
&ErrorDescription&string&/ErrorDescription&
&ErrorNumber&int&/ErrorNumber&
&CustomerOrderReference&long&/CustomerOrderReference&
&OrderConfirmation&string&/OrderConfirmation&
&CustomerDealRef&string&/CustomerDealRef&
&/CancelOrderResult&
&/CancelOrderResponse&
&/soap12:Body&
&/soap12:Envelope&
/cslunatic/item/d319b12d1e0df1bbb632633e
《-》Linux下用c语言实现发送http请求 方式可以Get或者Post ,下面是一个POST实例。
#include &stdio.h&
#include &sys/socket.h&
#include &sys/types.h&
#include &time.h&
#include &errno.h&
#include &signal.h&
#include &stdlib.h&
#include &string.h&
#include &unistd.h&
#include &sys/wait.h&
#include &sys/time.h&
#include &netinet/in.h&
#include &arpa/inet.h&
#define IPSTR &#.124.120&#8221;
#define PORT 80
#define BUFSIZE 1024
int main(intargc, char**argv)
intsockfd, ret, i,
structsockaddr_
charstr1[4096], str2[4096], buf[BUFSIZE], *
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) & 0 ) {
printf(&#8220;创建网络连接失败,本线程即将终止&#8212;socket error!\n&#8221;);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
if(inet_pton(AF_INET, IPSTR, &servaddr.sin_addr) &= 0 ){
printf(&#8220;创建网络连接失败,本线程即将终止&#8211;inet_pton error!\n&#8221;);
if(connect(sockfd, (structsockaddr *)&servaddr, sizeof(servaddr)) & 0){
printf(&#8220;连接到服务器失败,connect error!\n&#8221;);
printf(&#8220;与远端建立了连接\n&#8221;);
//发送数据
memset(str2, 0, 4096);
strcat(str2, &#8220;qqCode=&#8221;);
str=(char*)malloc(128);
len = strlen(str2);
sprintf(str, &#8220;%d&#8221;, len);
memset(str1, 0, 4096);
strcat(str1, &#8220;POST /webservices/qqOnlineWebService.asmx/qqCheckOnline HTTP/1.1\n&#8221;);
strcat(str1, &#8220;Host: .cn\n&#8221;);
strcat(str1, &#8220;Content-Type: application/x-www-form-urlencoded\n&#8221;);
strcat(str1, &#8220;Content-Length: &#8220;);
strcat(str1, str);
strcat(str1, &#8220;\n\n&#8221;);
strcat(str1, str2);
strcat(str1, &#8220;\r\n\r\n&#8221;);
printf(&#8220;%s\n&#8221;,str1);
ret = write(sockfd,str1,strlen(str1));
if(ret & 0) {
printf(&#8220;发送失败!错误代码是%d,错误信息是&#8217;%s&#8217;\n&#8221;,errno, strerror(errno));
printf(&#8220;消息发送成功,共发送了%d个字节!\n\n&#8221;, ret);
FD_ZERO(&t_set1);
FD_SET(sockfd, &t_set1);
tv.tv_sec= 0;
tv.tv_usec= 0;
printf(&#8220;&#8212;&#8212;&#8212;&#8212;&#21;);
h= select(sockfd +1, &t_set1, NULL, NULL, &tv);
printf(&#8220;&#8212;&#8212;&#8212;&#8212;&#21;);
//if (h == 0)
if(h & 0) {
close(sockfd);
printf(&#8220;在读取数据报文时SELECT检测到异常,该异常导致线程终止!\n&#8221;);
if(h & 0){
memset(buf, 0, 4096);
i= read(sockfd, buf, 4095);
close(sockfd);
printf(&#8220;读取数据报文时发现远端关闭,该线程终止!\n&#8221;);
printf(&#8220;%s\n&#8221;, buf);
close(sockfd);
《二》短信告警功能测试程序:(已经成功发送短信转发,与浏览器运行同效:/smsComputer/smsComputersend.asp?zh=&mm=&hm=&nr=tanqiuwei5445abc123&dxlbid=72)
&stdlib.h&
&string.h&
&stdarg.h&
&sys/socket.h&
&netinet/in.h&
&netdb.h&int htconnect(char
int white_
struct hostent
struct sockaddr_
site = gethostbyname(host);
if(site==NULL)
}white_sock = socket(AF_INET,SOCK_STREAM,0);
if(white_sock
printf(&#8220;创建网络连接失败,本线程即将终止&#8212;socket error!\n&#8221;);
memset(&me, 0, sizeof(struct sockaddr_in));
memcpy(&me.sin_addr, site-&h_addr_list[0], site-&h_length);
me.sin_family = AF_INET;
me.sin_port = htons(port);
return(connect(white_sock, (struct sockaddr*)&me,sizeof(struct sockaddr))&0)? -1:white_
int htsend(int sock, char *fmt, &#8230;)
BUF[1024];
va_start(argptr,fmt);
vsprintf(BUF,fmt,argptr);
va_end(argptr);
printf(BUF);
send(sock,BUF,strlen(BUF),0);
int main(int argc,char **argv)
int black_
msg[4096]={0};
request[4096] = {0};
black_sock = htconnect(&#8220;&#8221;,80);
if(black_sock &1)
printf(&#8220;连接到服务器失败,connect error!\n&#8221;);
return (-1);
char *URL = &#8220;/smsComputer/smsComputersend.asp&#8221;;
char *Content = &#8220;zh=&mm=&hm=&nr=tanqiuwei5445abc123&dxlbid=72 HTTP/1.1&#8221;;
char *Accept = &#8220;application/x-shockwave-flash, image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/xaml+xml, application/x-ms-xbap, application/x-ms-application, */*&#8221;;
char *Accept_language = &#8220;zh-cn&#8221;;
char *User_Agent = &#8220;Mozilla/4.0 ( MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.2; .NET CLR 2.0.50727; .NET4.0C; .NET4.0E)&#8221;;
char *Accept_Encoding = &#8220;gzip, deflate&#8221;;
char *Host = &#8220;&#8221;;
char *Connection = &#8220;Keep-Alive&#8221;;
//注意,HTTP消息后面结尾一定要用“\r\n\r\n”,不然就会返回“HTTP/1.1 400 Bad Request”
sprintf(request, &#8220;GET %s?%s\r\nAccept: %s\r\nAccept-Language: %s\r\nUser-Agent: %s\r\nAccept-Encoding: %s\r\nHost: %s\r\nConnection: %s\r\n\r\n&#8221;,
URL,Content, Accept, Accept_language,User_Agent,Accept_Encoding,Host,Connection);
printf(&#8220;%s&#8221;,request);
printf(&#8220;\n&#8221;);
send(black_sock, request,sizeof(request),0);
recv(black_sock,msg,sizeof(msg),0);
printf(&#8220;/n%s&#8221;,msg);
printf(&#8220;configure
success/n&#8221;);
close(black_sock);
return (-1);
网盘下载/s/1bYDXqm
在进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式:
同步/异步主要针对C端:
所谓同步,就是在c端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
例如普通B/S模式(同步):提交请求-&等待服务器处理-&处理完毕返回 这个期间客户端浏览器不能干任何事
异步的概念和同步相对。当c端一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
例如 ajax请求(异步): 请求通过事件触发-&服务器处理(这是浏览器仍然可以作其他事情)-&处理完毕
阻塞/非阻塞主要针对S端:
阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 例如,我们在socket中调用recv函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。
快递的例子:比如到你某个时候到A楼一层(假如是内核缓冲区)取快递,但是你不知道快递什么时候过来,你又不能干别的事,只能死等着。但你可以睡觉(进程处于休眠状态),因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
还是等快递的例子:如果用忙轮询的方法,每隔5分钟到A楼一层(内核缓冲区)去看快递来了没有。如果没来,立即返回。而快递来了,就放在A楼一层,等你去取。
对象的阻塞模式和阻塞函数调用
对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状 态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。
1. 同步,就是我客户端(c端调用者)调用一个功能,该功能没有结束前,我(c端调用者)死等结果。
2. 异步,就是我(c端调用者)调用一个功能,不需要知道该功能结果,该功能有结果后通知我(c端调用者)即回调通知。
同步/异步主要针对C端, 但是跟S端不是完全没有关系,同步/异步机制必须S端配合才能实现.同步/异步是由c端自己控制,但是S端是否阻塞/非阻塞, C端完全不需要关心.
就是调用我(s端被调用者,函数),我(s端被调用者,函数)没有接收完数据或者没有得到结果之前,我不会返回。
4. 非阻塞,
就是调用我(s端被调用者,函数),我(s端被调用者,函数)立即返回,通过select通知调用者
同步IO和异步IO的区别就在于:数据访问的时候进程是否阻塞!
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回!
同步和异步都只针对于本机SOCKET而言的。
同步和异步,阻塞和非阻塞,有些混用,其实它们完全不是一回事,而且它们修饰的对象也不相同。
阻塞和非阻塞是指当server端的进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪;
而同步和异步是指client端访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞(区别就绪与读写二个阶段,同步的读写必须阻塞),异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。(等待&#8221;通知&#8221;)
node.js里面的描述:
线程在执行中如果遇到磁盘读写或网络通信(统称为I/O 操作),通常要耗费较长的时间,这时操作系统会剥夺这个线程的CPU 控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程调度方式称为 阻塞。当I/O 操作完毕时,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种I/O 模式就是通常的同步式I/O(Synchronous I/O)或阻塞式I/O (Blocking I/O)。
相应地,异步式I/O (Asynchronous I/O)或非阻塞式I/O (Non-blocking I/O)则针对所有I/O 操作不采用阻塞的策略。当线程遇到I/O 操作时,不会以阻塞的方式等待I/O 操作的完成或数据的返回,而只是将I/O 请求发送给操作系统,继续执行下一条语句。当操作系统完成I/O 操作时,以事件的形式通知执行I/O 操作的线程,线程会在特定时候处理这个事件。为了处理异步I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予以处理。阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远在执行计算操作, style=&#8220;color:#ff0000;&#8221;这个线程所使用的CPU 核心利用率永远是100%,I/O 以事件的方式通知。 style=&#8220;color:#ff0000;&#8221;在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞时还有其他线程在工作,多线程可以让CPU 资源不被阻塞中的线程浪费。而在非阻塞模式下,线程不会被I/O 阻塞,永远在利用CPU。多线程带来的好处仅仅是在多核CPU 的情况下利用更多的核,而Node.js的单线程也能带来同样的好处。这就是为什么Node.js 使用了单线程、非阻塞的事件编程模式。
1)阻塞I/O(blocking I/O)
2)非阻塞I/O (nonblocking I/O)
3) I/O复用(select 和poll) (I/O multiplexing)
4)信号驱动I/O (signal driven I/O (SIGIO))
5)异步I/O (asynchronous I/O (the POSIX aio_functions))
前四种都是同步,只有最后一种才是异步IO。
阻塞I/O模型:
简介:进程会一直阻塞,直到数据拷贝完成
应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。
我们 第一次接触到的网络编程都是从 listen()、send()、recv()等接口开始的。使用这些接口可以很方便的构建服务器 /客户机的模型。
阻塞I/O模型图:在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程。
当调用recv()函数时,系统首先查是否有准备好的数据。如果数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,然后该函数返回。在套接应用程序中,当调用recv()函数时,未必用户空间就已经存在数据,那么此时recv()函数就会处于等待状态。
当使用socket()函数和WSASocket()函数创建套接字时,默认的套接字都是阻塞的。这意味着当调用Windows Sockets API不能立即完成时,线程处于等待状态,直到操作完成。
并不是所有Windows Sockets API以阻塞套接字为参数调用都会发生阻塞。例如,以阻塞模式的套接字为参数调用bind()、listen()函数时,函数会立即返回。将可能阻塞套接字的Windows Sockets API调用分为以下四种:
1.输入操作: recv()、recvfrom()、WSARecv()和WSARecvfrom()函数。以阻塞套接字为参数调用该函数接收数据。如果此时套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。
2.输出操作: send()、sendto()、WSASend()和WSASendto()函数。以阻塞套接字为参数调用该函数发送数据。如果套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。
3.接受连接:accept()和WSAAcept()函数。以阻塞套接字为参数调用该函数,等待接受对方的连接请求。如果此时没有连接请求,线程就会进入睡眠状态。
4.外出连接:connect()和WSAConnect()函数。对于TCP连接,客户端以阻塞套接字为参数,调用该函数向服务器发起连接。该函数在收到服务器的应答前,不会返回。这意味着TCP连接总会等待至少到服务器的一次往返时间。
使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下,使用阻塞模式来开发网络程序比较合适。
阻塞模式套接字的不足表现为,在大量建立好的套接字线程之间进行通信时比较困难。当使用“生产者-消费者”模型开发网络程序时,为每个套接字都分别分配一个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销。其最大的缺点是当希望同时处理大量套接字时,将无从下手,其扩展性很差.
阻塞模式给网络编程带来了一个很大的问题,如在调用 send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。这给多客户机、多业务逻辑的网络编程带来了挑战。这时,我们可能会选择多线程的方式来解决这个问题。
应对多客户机的网络应用,最简单的解决方式是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。
具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于线程,所以,如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的 CPU 资源,譬如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。通常,使用 pthread_create () 创建新线程,fork() 创建新进程。
多线程/进程服务器同时为多个客户机提供应答服务。模型如下:
主线程持续等待客户端的连接请求,如果有连接,则创建新线程,并在新线程中提供为前例同样的问答服务。
上述多线程的服务器模型似乎完美的解决了为多个客户机提供问答服务的要求,但其实并不尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。
由此可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如apache,mysql数据库等。
但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。
非阻塞IO模型 :
简介:非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;
我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。
把SOCKET设置为非阻塞模式,即通知系统内核:在调用Windows Sockets API时,不要让线程睡眠,而应该让函数立即返回。在返回时,该函数返回一个错误代码。图所示,一个非阻塞模式套接字多次调用recv()函数的过程。前三次调用recv()函数时,内核数据还没有准备好。因此,该函数立即返回WSAEWOULDBLOCK错误代码。第四次调用recv()函数时,数据已经准备好,被复制到应用程序的缓冲区中,recv()函数返回成功指示,应用程序开始处理数据。
当使用socket()函数和WSASocket()函数创建套接字时,默认都是阻塞的。在创建套接字之后,通过调用ioctlsocket()函数,将该套接字设置为非阻塞模式。Linux下的函数是:fcntl().
套接字设置为非阻塞模式后,在调用Windows Sockets API函数时,调用函数会立即返回。大多数情况下,这些函数调用都会调用“失败”,并返回WSAEWOULDBLOCK错误代码。说明请求的操作在调用期间内没有时间完成。通常,应用程序需要重复调用该函数,直到获得成功返回代码。
需要说明的是并非所有的Windows Sockets API在非阻塞模式下调用,都会返回WSAEWOULDBLOCK错误。例如,以非阻塞模式的套接字为参数调用bind()函数时,就不会返回该错误代码。当然,在调用WSAStartup()函数时更不会返回该错误代码,因为该函数是应用程序第一调用的函数,当然不会返回这样的错误代码。
要将套接字设置为非阻塞模式,除了使用ioctlsocket()函数之外,还可以使用WSAAsyncselect()和WSAEventselect()函数。当调用该函数时,套接字会自动地设置为非阻塞方式。
由于使用非阻塞套接字在调用函数时,会经常返回WSAEWOULDBLOCK错误。所以在任何时候,都应仔细检查返回代码并作好对“失败”的准备。应用程序连续不断地调用这个函数,直到它返回成功指示为止。上面的程序清单中,在While循环体内不断地调用recv()函数,以读入1024个字节的数据。这种做法很浪费系统资源。
要完成这样的操作,有人使用MSG_PEEK标志调用recv()函数查看缓冲区中是否有数据可读。同样,这种方法也不好。因为该做法对系统造成的开销是很大的,并且应用程序至少要调用recv()函数两次,才能实际地读入数据。较好的做法是,使用套接字的“I/O模型”来判断非阻塞套接字是否可读可写。
非阻塞模式套接字与阻塞模式套接字相比,不容易使用。使用非阻塞模式套接字,需要编写更多的代码,以便在每个Windows Sockets API函数调用中,对收到的WSAEWOULDBLOCK错误进行处理。因此,非阻塞套接字便显得有些难于使用。
但是,非阻塞套接字在控制建立的多个连接,在数据的收发量不均,时间不定时,明显具有优势。这种套接字在使用上存在一定难度,但只要排除了这些困难,它在功能上还是非常强大的。通常情况下,可考虑使用套接字的“I/O模型”,它有助于应用程序通过异步方式,同时对一个或多个套接字的通信加以管理。
IO复用模型:
简介:主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听;
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。
信号驱动IO
简介:两次调用,两次返回;
首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
异步IO模型
简介:数据拷贝的时候进程无需阻塞。
当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作
同步IO引起进程阻塞,直至IO操作完成。
异步IO不会引起进程阻塞。
IO复用是先通过select调用阻塞。
5个I/O模型的比较:
:http://blog.csdn.net/hguisu/article/details/
:http://blog.csdn.net/hguisu/article/details/
epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll:epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
epoll的优点:
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
select、poll、epoll 区别总结:
1、支持一个进程所能打开的最大连接数
单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接
2、FD剧增后带来的IO效率问题
因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。
3、 消息传递方式
内核需要将消息传递到用户空间,都需要内核拷贝动作
epoll通过内核和用户空间共享一块内存来实现的。
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。
1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善
同步/异步与阻塞/非阻塞经常看到是成对出现:
同步阻塞,异步非阻塞,同步非阻塞
进 程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进
程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如
UNIX BSD有:管道(pipe)、命名管道(named pipe)软中断信号(signal)
UNIX system V有:消息(message)、共享存储区(shared memory)和信号量(semaphore)等.
他们都仅限于用在本机进程之间通信。网间进程通信要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例)。为此,首先要解决的是网间进程标识问题。同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋于某进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。 其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。
其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX
BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
TCP/IP协议存在于OS中,网络服务通过OS提供,在OS中增加支持TCP/IP的系统调用——Berkeley套接字,如Socket,Connect,Send,Recv等
UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。如图:
TCP/IP协议族包括运输层、网络层、链路层,而socket所在位置如图,Socket是应用层与TCP/IP协议族通信的中间软件抽象层。
1、 socket套接字:
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –& 读写write/read –& 关闭close”模式来操作。Socket就是该模式的一个实现,
socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭).
说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
注意:其实socket也没有层的概念,它只是一个facade设计模式的应用,让编程变的更简单。是一个软件抽象层。在网络编程中,我们大量用的都是通过socket实现的。
2、套接字描述符
其实就是一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr
套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。特别是,当应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字。然后,应用程序以该描述符作为传递参数,通过调用函数来完成某种操作(例如通过网络传送数据或接收输入的数据)。
在许多操作系统中,套接字描述符和其他I/O描述符是集成在一起的,所以应用程序可以对文件进行套接字I/O或I/O读/写操作。
当应用程序要创建一个套接字时,操作系统就返回一个小整数作为描述符,应用程序则使用这个描述符来引用该套接字需要I/O请求的应用程序请求操作系统打开一个文件。操作系统就创建一个文件描述符提供给应用程序访问文件。从应用程序的角度看,文件描述符是一个整数,应用程序可以用它来读写文件。下图显示,操作系统如何把文件描述符实现为一个指针数组,这些指针指向内部数据结构。
对于每个程序系统都有一张单独的表。精确地讲,系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者 。应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。
针对套接字的系统数据结构:
1)、套接字API里有个函数socket,它就是用来创建一个套接字。套接字设计的总体思路是,单个系统调用就可以创建任何套接字,因为套接字是相当笼统的。一旦套接字创建后,应用程序还需要调用其他函数来指定具体细节。例如调用socket将创建一个新的描述符条目:
2)、虽然套接字的内部数据结构包含很多字段,但是系统创建套接字后,大多数字字段没有填写。应用程序创建套接字后在该套接字可以使用之前,必须调用其他的过程来填充这些字段。
3、文件描述符和文件指针的区别:
文件描述符:在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。
文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。
详细内容请看:
在生活中,A要电话给B,A拨号,B听到电话铃声后提起电话,这时A和B就建立起了连接,A和B就可以讲话了。等交流结束,挂断电话结束此次交谈。
打电话很简单解释了这工作原理:“open—write/read—close”模式。
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
这些接口的实现都是内核来完成。具体如何实现,可以看看linux的内核
4.1、socket()函数
(int protofamily, int type, int protocol);//返回sockfd
sockfd是描述符。
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
protofamily:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
4.2、bind()函数
正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数的三个参数分别为:
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
struct sockaddr_in {
sa_family_t
sin_ /* address family: AF_INET */
/* port in network byte order */
struct in_addr sin_
/* internet address */
/* Internet address. */
struct in_addr {
/* address in network byte order */
ipv6对应的是:
struct sockaddr_in6 {
sa_family_t
/* AF_INET6 */
/* port number */
sin6_ /* IPv6 flow information */
struct in6_addr sin6_
/* IPv6 address */
sin6_scope_ /* Scope ID (new in 2.4) */
struct in6_addr {
unsigned char
s6_addr[16];
/* IPv6 address */
Unix域对应的是:
#define UNIX_PATH_MAX
struct sockaddr_un {
sa_family_t sun_
/* AF_UNIX */
sun_path[UNIX_PATH_MAX];
/* pathname */
addrlen:对应的是地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
网络字节序与主机字节序
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。
4.3、listen()、connect()函数
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
4.4、accept()函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd
参数sockfd
参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。
这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
如同大家所认为的,它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。
如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。
accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。
此时我们需要区分两种套接字,
监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)
连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。
一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
自然要问的是:为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。
连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号
4.5、read()、write()等函数
万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:
#include &unistd.h&
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include &sys/types.h&
#include &sys/socket.h&
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。
其它的我就不一一介绍这几对I/O函数了,具体参见man文档或者baidu、Google,下面的例子中将使用到send/recv。
4.6、close()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
#include &unistd.h&
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
TCP协议通过三个报文段完成连接的建立,这个过程称为三次握手(three-way handshake),过程如下图所示。
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
一个完整的三次握手也就是: 请求&#8212;应答&#8212;再次确认。
对应的函数接口:
从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
我们可以通过网络抓包的查看具体的流程:
比如我们服务器开启9502的端口。使用tcpdump来抓包:
tcpdump -iany tcp port 9502
然后我们使用telnet 127.0.0.1 9502开连接.:
telnet 127.0.0.1 9502
14:12:45.104687 IP localhost.39870 & localhost.9502: Flags [S], seq , win 32792, options [mss 16396,sackOK,TS val
ecr 0,nop,wscale 3], length 0(1)
14:12:45.104701 IP localhost.9502 & localhost.39870: Flags [S.], seq , ack , win 32768, options [mss 16396,sackOK,TS val
ecr ,nop,wscale 3], length 0
14:12:45.104711 IP localhost.39870 & localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val
ecr ], length 0
14:13:01.415407 IP localhost.39870 & localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val
ecr ], length 7
14:13:01.415432 IP localhost.9502 & localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val
ecr ], length 0
14:13:01.415747 IP localhost.9502 & localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val
ecr ], length 18
14:13:01.415757 IP localhost.39870 & localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val
ecr ], length 0
114:12:45.104687 时间带有精确到微妙
localhost.39870 & localhost.9502 表示通信的流向,39870是客户端,9502是服务器端
[S] 表示这是一个SYN请求
[S.] 表示这是一个SYN+ACK确认包:
[.] 表示这是一个ACT确认包, (client)SYN-&(server)SYN-&(client)ACT 就是3次握手过程
[P] 表示这个是一个数据推送,可以是从服务器端向客户端推送,也可以从客户端向服务器端推
[F] 表示这是一个FIN包,是关闭连接操作,client/server都有可能发起
[R] 表示这是一个RST包,与F包作用相同,但RST表示连接关闭时,仍然有数据未被处理。可以理解为是强制切断连接
win 4099 是指滑动窗口大小
length 18指数据包的大小
我们看到 (1)(2)(3)三步是建立tcp:
第一次握手:
14:12:45.104687 IP localhost.39870 & localhost.9502: Flags [S], seq
客户端IP localhost.39870 (客户端的端口一般是自动分配的) 向服务器localhost.9502 发送syn包(syn=j)到服务器》
syn包(syn=j) : syn的seq=
第二次握手:
14:12:45.104701 IP localhost.9502 & localhost.39870: Flags [S.], seq , ack ,
收到请求并确认:服务器收到syn包,并必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包:
此时服务器主机自己的SYN:seq:y= syn seq 。
ACK为j+1 =(ack=j+1)=ack
第三次握手:
14:12:45.104711 IP localhost.39870 & localhost.9502: Flags [.], ack 1,
客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1)
客户端和服务器进入ESTABLISHED状态后,可以进行通信数据交互。此时和accept接口没有关系,即使没有accepte,也进行3次握手完成。
连接出现连接不上的问题,一般是网路出现问题或者网卡超负荷或者是连接数已经满啦。
紫色背景的部分:
IP localhost.39870 & localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val
ecr ], length 7
客户端向服务器发送长度为7个字节的数据,
IP localhost.9502 & localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val
ecr ], length 0
服务器向客户确认已经收到数据
IP localhost.9502 & localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val
ecr ], length 18
然后服务器同时向客户端写入数据。
IP localhost.39870 & localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val
ecr ], length 0
客户端向服务器确认已经收到数据
这个就是tcp可靠的连接,每次通信都需要对方来确认。
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。
(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。
(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。
对应函数接口如图:
过程如下:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息。
#include&stdio.h&
#include&stdlib.h&
#include&string.h&
#include&errno.h&
#include&sys/types.h&
#include&sys/socket.h&
#include&netinet/in.h&
#define DEFAULT_PORT 8000
#define MAXLINE 4096
int main(int argc, char** argv)
socket_fd, connect_
struct sockaddr_
buff[4096];
if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
printf(&#8220;create socket error: %s(errno: %d)\n&#8221;,strerror(errno),errno);
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(DEFAULT_PORT);
if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
printf(&#8220;bind socket error: %s(errno: %d)\n&#8221;,strerror(errno),errno);
if( listen(socket_fd, 10) == -1){
printf(&#8220;listen socket error: %s(errno: %d)\n&#8221;,strerror(errno),errno);
printf(&#8220;======waiting for client&#8217;s request======\n&#8221;);
if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){
printf(&#8220;accept socket error: %s(errno: %d)&#8221;,strerror(errno),errno);
n = recv(connect_fd, buff, MAXLINE, 0);
if(!fork()){
if(send(connect_fd, &#8220;Hello,you are connected!\n&#8221;, 26,0) == -1)
perror(&#8220;send error&#8221;);
close(connect_fd);
buff[n] = &#17;;
printf(&#8220;recv msg from client: %s\n&#8221;, buff);
close(connect_fd);
close(socket_fd);
#include&stdio.h&
#include&stdlib.h&
#include&string.h&
#include&errno.h&
#include&sys/types.h&
#include&sys/socket.h&
#include&netinet/in.h&
#define MAXLINE 4096
int main(int argc, char** argv)
sockfd, n,rec_
recvline[4096], sendline[4096];
buf[MAXLINE];
struct sock}

我要回帖

更多关于 工作以后考研究生 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信