精华内容
下载资源
问答
  • 网络编程面试题(2020最新版)

    万次阅读 多人点赞 2020-03-16 17:36:28
    为什么网络协议分层?TCP/IP 协议族应用层运输层网络层数据链路层物理层TCP/IP 协议族TCP的三次握手四次挥手TCP报文的头部结构三次握手四次挥手常见面试题为什么TCP连接的时候是3次?2次不可以吗?为什么TCP连接...

    大家好,我是CSDN的博主ThinkWon,“2020博客之星年度总评选"开始啦,希望大家帮我投票,每天都可以投多票哦,点击下方链接,然后点击"最大”,再点击"投TA一票"就可以啦!
    投票链接:https://bss.csdn.net/m/topic/blog_star2020/detail?username=thinkwon
    在技术的世界里,ThinkWon将一路与你相伴!创作出更多更高质量的文章!2020为努力奋斗的你点赞👍,️新的一年,祝各位大牛牛气冲天,牛年大吉!😊😊

    计算机网络体系结构

    在计算机网络的基本概念中,分层次的体系结构是最基本的。计算机网络体系结构的抽象概念较多,在学习时要多思考。这些概念对后面的学习很有帮助。

    网络协议是什么?

    在计算机网络要做到有条不紊地交换数据,就必须遵守一些事先约定好的规则,比如交换数据的格式、是否需要发送一个应答信息。这些规则被称为网络协议。

    为什么要对网络协议分层?

    • 简化问题难度和复杂度。由于各层之间独立,我们可以分割大问题为小问题。
    • 灵活性好。当其中一层的技术变化时,只要层间接口关系保持不变,其他层不受影响。
    • 易于实现和维护。
    • 促进标准化工作。分开后,每层功能可以相对简单地被描述。

    网络协议分层的缺点: 功能可能出现在多个层里,产生了额外开销。

    为了使不同体系结构的计算机网络都能互联,国际标准化组织 ISO 于1977年提出了一个试图使各种计算机在世界范围内互联成网的标准框架,即著名的开放系统互联基本参考模型 OSI/RM,简称为OSI。

    OSI 的七层协议体系结构的概念清楚,理论也较完整,但它既复杂又不实用,TCP/IP 体系结构则不同,但它现在却得到了非常广泛的应用。TCP/IP 是一个四层体系结构,它包含应用层,运输层,网际层和网络接口层(用网际层这个名字是强调这一层是为了解决不同网络的互连问题),不过从实质上讲,TCP/IP 只有最上面的三层,因为最下面的网络接口层并没有什么具体内容,因此在学习计算机网络的原理时往往采用折中的办法,即综合 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚,有时为了方便,也可把最底下两层称为网络接口层。

    四层协议,五层协议和七层协议的关系如下:

    • TCP/IP是一个四层的体系结构,主要包括:应用层、运输层、网际层和网络接口层。
    • 五层协议的体系结构主要包括:应用层、运输层、网络层,数据链路层和物理层。
    • OSI七层协议模型主要包括是:应用层(Application)、表示层(Presentation)、会话层(Session)、运输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)。

    在这里插入图片描述

    注:五层协议的体系结构只是为了介绍网络原理而设计的,实际应用还是 TCP/IP 四层体系结构。

    TCP/IP 协议族

    应用层

    应用层( application-layer )的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。

    对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统 DNS,支持万维网应用的 HTTP 协议,支持电子邮件的 SMTP 协议等等。

    运输层

    运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。

    运输层主要使用一下两种协议

    1. 传输控制协议-TCP:提供面向连接的,可靠的数据传输服务。
    2. 用户数据协议-UDP:提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。
    UDP TCP
    是否连接 无连接 面向连接
    是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输,使用流量控制和拥塞控制
    连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
    传输方式 面向报文 面向字节流
    首部开销 首部开销小,仅8字节 首部最小20字节,最大60字节
    场景 适用于实时应用(IP电话、视频会议、直播等) 适用于要求可靠传输的应用,例如文件传输

    每一个应用层(TCP/IP参考模型的最高层)协议一般都会使用到两个传输层协议之一:

    运行在TCP协议上的协议:

    • HTTP(Hypertext Transfer Protocol,超文本传输协议),主要用于普通浏览。
    • HTTPS(HTTP over SSL,安全超文本传输协议),HTTP协议的安全版本。
    • FTP(File Transfer Protocol,文件传输协议),用于文件传输。
    • POP3(Post Office Protocol, version 3,邮局协议),收邮件用。
    • SMTP(Simple Mail Transfer Protocol,简单邮件传输协议),用来发送电子邮件。
    • TELNET(Teletype over the Network,网络电传),通过一个终端(terminal)登陆到网络。
    • SSH(Secure Shell,用于替代安全性差的TELNET),用于加密安全登陆用。

    运行在UDP协议上的协议:

    • BOOTP(Boot Protocol,启动协议),应用于无盘设备。
    • NTP(Network Time Protocol,网络时间协议),用于网络同步。
    • DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),动态配置IP地址。

    运行在TCPUDP协议上:

    • DNS(Domain Name Service,域名服务),用于完成地址查找,邮件转发等工作。

    网络层

    网络层的任务就是选择合适的网间路由和交换结点,确保计算机通信的数据及时传送。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称数据报。

    互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Prococol)和许多路由选择协议,因此互联网的网络层也叫做网际层或 IP 层。

    数据链路层

    数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。

    在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。

    在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。

    一般的web应用的通信传输流是这样的:

    img

    发送端在层与层之间传输数据时,每经过一层时会被打上一个该层所属的首部信息。反之,接收端在层与层之间传输数据时,每经过一层时会把对应的首部信息去除。

    物理层

    在物理层上所传送的数据单位是比特。 物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。

    TCP/IP 协议族

    在互联网使用的各种协议中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的 TCP/IP 并不一定是单指 TCP 和 IP 这两个具体的协议,而往往是表示互联网所使用的整个 TCP/IP 协议族。

    img

    互联网协议套件(英语:Internet Protocol Suite,缩写IPS)是一个网络通讯模型,以及一整个网络传输协议家族,为网际网络的基础通讯架构。它常被通称为TCP/IP协议族(英语:TCP/IP Protocol Suite,或TCP/IP Protocols),简称TCP/IP。因为该协定家族的两个核心协定:TCP(传输控制协议)和IP(网际协议),为该家族中最早通过的标准。

    划重点:

    TCP(传输控制协议)和IP(网际协议) 是最先定义的两个核心协议,所以才统称为TCP/IP协议族

    TCP的三次握手四次挥手

    TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务端保存的一份关于对方的信息,如ip地址、端口号等。

    TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在TCP头部。

    一个TCP连接由一个4元组构成,分别是两个IP地址和两个端口号。一个TCP连接通常分为三个阶段:连接、数据传输、退出(关闭)。通过三次握手建立一个链接,通过四次挥手来关闭一个连接

    当一个连接被建立或被终止时,交换的报文段只包含TCP头部,而没有数据

    TCP报文的头部结构

    在了解TCP连接之前先来了解一下TCP报文的头部结构。

    TCPHeader.png

    上图中有几个字段需要重点介绍下:

    (1)序号:seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。

    (2)确认序号:ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1。

    (3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:

    • ACK:确认序号有效。
    • FIN:释放一个连接。
    • PSH:接收方应该尽快将这个报文交给应用层。
    • RST:重置连接。
    • SYN:发起一个新连接。
    • URG:紧急指针(urgent pointer)有效。

    需要注意的是:

    • 不要将确认序号ack与标志位中的ACK搞混了。
    • 确认方ack=发起方seq+1,两端配对。

    三次握手

    三次握手的本质是确认通信双方收发数据的能力

    首先,我让信使运输一份信件给对方,对方收到了,那么他就知道了我的发件能力和他的收件能力是可以的

    于是他给我回信,我若收到了,我便知我的发件能力和他的收件能力是可以的,并且他的发件能力和我的收件能力是可以

    然而此时他还不知道他的发件能力和我的收件能力到底可不可以,于是我最后回馈一次,他若收到了,他便清楚了他的发件能力和我的收件能力是可以的

    这,就是三次握手,这样说,你理解了吗?

    三次握手.png

    • 第一次握手:客户端要向服务端发起连接请求,首先客户端随机生成一个起始序列号ISN(比如是100),那客户端向服务端发送的报文段包含SYN标志位(也就是SYN=1),序列号seq=100。
    • 第二次握手:服务端收到客户端发过来的报文后,发现SYN=1,知道这是一个连接请求,于是将客户端的起始序列号100存起来,并且随机生成一个服务端的起始序列号(比如是300)。然后给客户端回复一段报文,回复报文包含SYN和ACK标志(也就是SYN=1,ACK=1)、序列号seq=300、确认号ack=101(客户端发过来的序列号+1)。
    • 第三次握手:客户端收到服务端的回复后发现ACK=1并且ack=101,于是知道服务端已经收到了序列号为100的那段报文;同时发现SYN=1,知道了服务端同意了这次连接,于是就将服务端的序列号300给存下来。然后客户端再回复一段报文给服务端,报文包含ACK标志位(ACK=1)、ack=301(服务端序列号+1)、seq=101(第一次握手时发送报文是占据一个序列号的,所以这次seq就从101开始,需要注意的是不携带数据的ACK报文是不占据序列号的,所以后面第一次正式发送数据时seq还是101)。当服务端收到报文后发现ACK=1并且ack=301,就知道客户端收到序列号为300的报文了,就这样客户端和服务端通过TCP建立了连接。

    四次挥手

    四次挥手的目的是关闭一个连接

    四次挥手.jpeg

    比如客户端初始化的序列号ISA=100,服务端初始化的序列号ISA=300。TCP连接成功后客户端总共发送了1000个字节的数据,服务端在客户端发FIN报文前总共回复了2000个字节的数据。

    • 第一次挥手:当客户端的数据都传输完成后,客户端向服务端发出连接释放报文(当然数据没发完时也可以发送连接释放报文并停止发送数据),释放连接报文包含FIN标志位(FIN=1)、序列号seq=1101(100+1+1000,其中的1是建立连接时占的一个序列号)。需要注意的是客户端发出FIN报文段后只是不能发数据了,但是还可以正常收数据;另外FIN报文段即使不携带数据也要占据一个序列号。
    • 第二次挥手:服务端收到客户端发的FIN报文后给客户端回复确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=1102(客户端FIN报文序列号1101+1)、序列号seq=2300(300+2000)。此时服务端处于关闭等待状态,而不是立马给客户端发FIN报文,这个状态还要持续一段时间,因为服务端可能还有数据没发完。
    • 第三次挥手:服务端将最后数据(比如50个字节)发送完毕后就向客户端发出连接释放报文,报文包含FIN和ACK标志位(FIN=1,ACK=1)、确认号和第二次挥手一样ack=1102、序列号seq=2350(2300+50)。
    • 第四次挥手:客户端收到服务端发的FIN报文后,向服务端发出确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=2351、序列号seq=1102。注意客户端发出确认报文后不是立马释放TCP连接,而是要经过2MSL(最长报文段寿命的2倍时长)后才释放TCP连接。而服务端一旦收到客户端发出的确认报文就会立马释放TCP连接,所以服务端结束TCP连接的时间要比客户端早一些。

    常见面试题

    为什么TCP连接的时候是3次?2次不可以吗?

    因为需要考虑连接时丢包的问题,如果只握手2次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数(可以理解服务端已经连接成功)据,而客户端一直没收到服务端的确认报文,所以客户端就不知道服务端是否已经准备好了(可以理解为客户端未连接成功),这种情况下客户端不会给服务端发数据,也会忽略服务端发过来的数据。

    如果是三次握手,即便发生丢包也不会有问题,比如如果第三次握手客户端发的确认ack报文丢失,服务端在一段时间内没有收到确认ack报文的话就会重新进行第二次握手,也就是服务端会重发SYN报文段,客户端收到重发的报文段后会再次给服务端发送确认ack报文。

    为什么TCP连接的时候是3次,关闭的时候却是4次?

    因为只有在客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了,服务端还有没有数据发客户端是不知道的。而服务端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了,但我服务端还有一些数据没发完,等这些数据发完了服务端才能给客户端发FIN报文(所以不能一次性将确认报文和FIN报文发给客户端,就是这里多出来了一次)。

    为什么客户端发出第四次挥手的确认报文后要等2MSL的时间才能释放TCP连接?

    这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。

    如果已经建立了连接,但是客户端突然出现故障了怎么办?

    TCP设有一个保活计时器,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

    什么是HTTP,HTTP 与 HTTPS 的区别

    HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范

    区别 HTTP HTTPS
    协议 运行在 TCP 之上,明文传输,客户端与服务器端都无法验证对方的身份 身披 SSL( Secure Socket Layer )外壳的 HTTP,运行于 SSL 上,SSL 运行于 TCP 之上, 是添加了加密和认证机制的 HTTP
    端口 80 443
    资源消耗 较少 由于加解密处理,会消耗更多的 CPU 和内存资源
    开销 无需证书 需要证书,而证书一般需要向认证机构购买
    加密机制 共享密钥加密和公开密钥加密并用的混合加密机制
    安全性 由于加密机制,安全性强

    常用HTTP状态码

    HTTP状态码表示客户端HTTP请求的返回结果、标识服务器处理是否正常、表明请求出现的错误等。

    状态码的类别:

    类别 原因短语
    1XX Informational(信息性状态码) 接受的请求正在处理
    2XX Success(成功状态码) 请求正常处理完毕
    3XX Redirection(重定向状态码) 需要进行附加操作以完成请求
    4XX Client Error(客户端错误状态码) 服务器无法处理请求
    5XX Server Error(服务器错误状态码) 服务器处理请求出错

    常用HTTP状态码:

    2XX 成功(这系列表明请求被正常处理了)
    200 OK,表示从客户端发来的请求在服务器端被正确处理
    204 No content,表示请求成功,但响应报文不含实体的主体部分
    206 Partial Content,进行范围请求成功
    3XX 重定向(表明浏览器要执行特殊处理)
    301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
    302 found,临时性重定向,表示资源临时被分配了新的 URL
    303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源(对于301/302/303响应,几乎所有浏览器都会删除报文主体并自动用GET重新请求)
    304 not modified,表示服务器允许访问资源,但请求未满足条件的情况(与重定向无关)
    307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求
    4XX 客户端错误
    400 bad request,请求报文存在语法错误
    401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
    403 forbidden,表示对请求资源的访问被服务器拒绝,可在实体主体部分返回原因描述
    404 not found,表示在服务器上没有找到请求的资源
    5XX 服务器错误
    500 internal sever error,表示服务器端在执行请求时发生了错误
    501 Not Implemented,表示服务器不支持当前请求所需要的某个功能
    503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

    GET和POST区别

    说道GET和POST,就不得不提HTTP协议,因为浏览器和服务器的交互是通过HTTP协议执行的,而GET和POST也是HTTP协议中的两种方法。

    HTTP全称为Hyper Text Transfer Protocol,中文翻译为超文本传输协议,目的是保证浏览器与服务器之间的通信。HTTP的工作方式是客户端与服务器之间的请求-应答协议。

    HTTP协议中定义了浏览器和服务器进行交互的不同方法,基本方法有4种,分别是GET,POST,PUT,DELETE。这四种方法可以理解为,对服务器资源的查,改,增,删。

    • GET:从服务器上获取数据,也就是所谓的查,仅仅是获取服务器资源,不进行修改。
    • POST:向服务器提交数据,这就涉及到了数据的更新,也就是更改服务器的数据。
    • PUT:英文含义是放置,也就是向服务器新添加数据,就是所谓的增。
    • DELETE:从字面意思也能看出,这种方式就是删除服务器数据的过程。

    GET和POST区别

    1. Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的。 但是这种做法也不时绝对的,大部分人的做法也是按照上面的说法来的,但是也可以在get请求加上 request body,给 post请求带上 URL 参数。

    2. Get请求提交的url中的数据最多只能是2048字节,这个限制是浏览器或者服务器给添加的,http协议并没有对url长度进行限制,目的是为了保证服务器和浏览器能够正常运行,防止有人恶意发送请求。Post请求则没有大小限制。

    3. Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集。

    4. Get执行效率却比Post方法好。Get是form提交的默认方法。

    5. GET产生一个TCP数据包;POST产生两个TCP数据包。

      对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

      而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

    什么是对称加密与非对称加密

    对称密钥加密是指加密和解密使用同一个密钥的方式,这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方;

    而非对称加密是指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。
    由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,非常的慢

    什么是HTTP2

    HTTP2 可以提高了网页的性能。

    在 HTTP1 中浏览器限制了同一个域名下的请求数量(Chrome 下一般是六个),当在请求很多资源的时候,由于队头阻塞当浏览器达到最大请求数量时,剩余的资源需等待当前的六个请求完成后才能发起请求。

    HTTP2 中引入了多路复用的技术,这个技术可以只通过一个 TCP 连接就可以传输所有的请求数据。多路复用可以绕过浏览器限制同一个域名下的请求数量的问题,进而提高了网页的性能。

    Session、Cookie和Token的主要区别

    HTTP协议本身是无状态的。什么是无状态呢,即服务器无法判断用户身份。

    什么是cookie

    cookie是由Web服务器保存在用户浏览器上的小文件(key-value格式),包含用户相关的信息。客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户身份。

    什么是session

    session是依赖Cookie实现的。session是服务器端对象

    session 是浏览器和服务器会话过程中,服务器分配的一块储存空间。服务器默认为浏览器在cookie中设置 sessionid,浏览器在向服务器请求过程中传输 cookie 包含 sessionid ,服务器根据 sessionid 获取出会话中存储的信息,然后确定会话的身份信息。

    cookie与session区别

    • 存储位置与安全性:cookie数据存放在客户端上,安全性较差,session数据放在服务器上,安全性相对更高;
    • 存储空间:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,session无此限制
    • 占用服务器资源:session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。

    什么是Token

    Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。

    Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

    使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。

    Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位

    session与token区别

    • session机制存在服务器压力增大,CSRF跨站伪造请求攻击,扩展性不强等问题;
    • session存储在服务器端,token存储在客户端
    • token提供认证和授权功能,作为身份认证,token安全性比session好;
    • session这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上,token适用于项目级的前后端分离(前后端代码运行在不同的服务器下)

    Servlet是线程安全的吗

    Servlet不是线程安全的,多线程并发的读写会导致数据不同步的问题。

    解决的办法是尽量不要定义name属性,而是要把name变量分别定义在doGet()和doPost()方法内。虽然使用synchronized(name){}语句块可以解决问题,但是会造成线程的等待,不是很科学的办法。

    注意:多线程的并发的读写Servlet类属性会导致数据不同步。但是如果只是并发地读取属性而不写入,则不存在数据不同步的问题。因此Servlet里的只读属性最好定义为final类型的。

    Servlet接口中有哪些方法及Servlet生命周期探秘

    在Java Web程序中,Servlet主要负责接收用户请求HttpServletRequest,在doGet()doPost()中做相应的处理,并将回应HttpServletResponse反馈给用户。Servlet可以设置初始化参数,供Servlet内部使用。

    Servlet接口定义了5个方法,其中前三个方法与Servlet生命周期相关

    • void init(ServletConfig config) throws ServletException
    • void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException
    • void destory()
    • java.lang.String getServletInfo()
    • ServletConfig getServletConfig()

    生命周期:

    Web容器加载Servlet并将其实例化后,Servlet生命周期开始,容器运行其init()方法进行Servlet的初始化;

    请求到达时调用Servlet的service()方法,service()方法会根据需要调用与请求对应的doGet或doPost等方法;

    当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的destroy()方法

    init方法和destory方法只会执行一次,service方法客户端每次请求Servlet都会执行。Servlet中有时会用到一些需要初始化与销毁的资源,因此可以把初始化资源的代码放入init方法中,销毁资源的代码放入destroy方法中,这样就不需要每次处理客户端的请求都要初始化与销毁资源。

    如果客户端禁止 cookie 能实现 session 还能用吗?

    Cookie 与 Session,一般认为是两个独立的东西,Session采用的是在服务器端保持状态的方案,而Cookie采用的是在客户端保持状态的方案。

    但为什么禁用Cookie就不能得到Session呢?因为Session是用Session ID来确定当前对话所对应的服务器Session,而Session ID是通过Cookie来传递的,禁用Cookie相当于失去了Session ID,也就得不到Session了。

    假定用户关闭Cookie的情况下使用Session,其实现途径有以下几种:

    1. 手动通过URL传值、隐藏表单传递Session ID。
    2. 用文件、数据库等形式保存Session ID,在跨页过程中手动调用。
    展开全文
  • QT之网络编程错误总结

    千次阅读 2015-07-26 21:28:14
    一、 QObject::connect: Cannot queue arguments of type 'QAbstractSocket::SocketError' (Make sure 'QAbstractSocket::SocketError' is registered using qRegisterMetaType...错误描述:在QThread内将QTcp

    一、

    QObject::connect: Cannot queue arguments of type 'QAbstractSocket::SocketError'
    (Make sure 'QAbstractSocket::SocketError' is registered using qRegisterMetaType().)


    错误描述:在QThread内将QTcpSocket对象信号(connected、error、stateChangeed)与槽函数连接报数据类型未注册。


    报错原因:connect自动连接在线程类中的信号默认是排队模式QueuedConnection,因此需要注册参数类型。


    解决方法:将默认connect的连接方式改为Qt::DirectConnection,就不需要类型信息。



    QT信号与槽的连接方式:   
    Qt::AutoConnection表示系统自动选择相应的连接方式,如果信号与槽在同一线程,就采用Qt::DirectConnection,如果信号与槽不在同一线程,将采用Qt::QueuedConnection的连接方式。   
    Qt::DirectConnection表示一旦信号产生,立即执行槽函数。   
    Qt::QueuedConnection表示信号产生后,将发送Event给你的receiver所在的线程,postEvent(QEvent::MetaCall,...),slot函数会在receiver所在的线程的event loop中进行处理。   
    Qt::BlockingQueuedConnection表示信号产生后调用sendEvent(QEvent::MetaCall,...),在receiver所在的线程处理完成后才会返回,只能当sender,receiver不在同一线程时才可以。   
    Qt::UniqueConnection表示只有它不是一个重复连接,连接才会成功。如果之前已经有了一个链接(相同的信号连接到同一对象的同一个槽上),那么连接将会失败并将返回false。   

    展开全文
  • 网络编程学习笔记一:Socket编程

    万次阅读 多人点赞 2013-03-21 01:11:11
    话虽些许夸张,但是事实也是,现在的网络编程几乎都是用的socket。 ——有感于实际编程和开源项目研究。 我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与...

    “一切皆Socket!”

    话虽些许夸张,但是事实也是,现在的网络编程几乎都是用的socket。

    ——有感于实际编程和开源项目研究。

    我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那什么是socket?socket的类型有哪些?还有socket的基本函数,这些都是本文想介绍的。本文的主要内容如下:

    • 1、网络中进程之间如何通信?
    • 2、Socket是什么?
    • 3、socket的基本操作
      • 3.1、socket()函数
      • 3.2、bind()函数
      • 3.3、listen()、connect()函数
      • 3.4、accept()函数
      • 3.5、read()、write()函数等
      • 3.6、close()函数
    • 4、socket中TCP的三次握手建立连接详解
    • 5、socket中TCP的四次握手释放连接详解
    • 6、一个例子(实践一下)
    • 7、留下一个问题,欢迎大家回帖回答!!!

    1、网络中进程之间如何通信?

    本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:

    • 消息传递(管道、FIFO、消息队列)
    • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
    • 共享内存(匿名的和具名的)
    • 远程过程调用(Solaris门和Sun RPC)

    但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址可以唯一标识网络中的主机,而传输层的“协议+端口可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

    使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX  BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。

    2、什么是Socket?

    上面我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。

    socket一词的起源

    在组网领域的首次使用是在1970年2月12日发布的文献IETF RFC33中发现的,撰写者为Stephen Carr、Steve Crocker和Vint Cerf。根据美国计算机历史博物馆的记载,Croker写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大约12年。”

    3、socket的基本操作

    既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。

    3.1、socket()函数

    int socket(int domain, int type, int protocol);

     socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

    正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

    • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INETAF_INET6AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
    • type:指定socket类型。常用的socket类型有,SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等等(socket的类型有哪些?)。
    • protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。

    注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

    当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()listen()时系统会自动随机分配一个端口。

    3.2、bind()函数

    正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INETAF_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_family; /* address family: AF_INET */
          in_port_t      sin_port;   /* port in network byte order */
          struct in_addr sin_addr;   /* internet address */
      };
      
      /* Internet address. */
      struct in_addr {
          uint32_t       s_addr;     /* address in network byte order */
      };

      ipv6对应的是: 

      struct sockaddr_in6 { 
          sa_family_t     sin6_family;   /* AF_INET6 */ 
          in_port_t       sin6_port;     /* port number */ 
          uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
          struct in6_addr sin6_addr;     /* IPv6 address */ 
          uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
      };
      
      struct in6_addr { 
          unsigned char   s6_addr[16];   /* IPv6 address */ 
      };

      Unix域对应的是: 

      #define UNIX_PATH_MAX    108
      
      struct sockaddr_un { 
          sa_family_t sun_family;               /* AF_UNIX */ 
          char        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。

    3.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服务器的连接。

    3.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);

    accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

    注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

    3.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。

    3.6、close()函数

    在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

    #include <unistd.h>
    int close(int fd);

    close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

    注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

    4、socket中TCP的三次握手建立连接详解

    我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

    • 客户端向服务器发送一个SYN J
    • 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
    • 客户端再想服务器发一个确认ACK K+1

    只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:

    image

    图1、socket中发送的TCP三次握手

    从图中可以看出,当客户端调用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返回,至此三次握手完毕,连接建立。

    总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。

    5、socket中TCP的四次握手释放连接详解

    上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:

    image

    图2、socket中发送的TCP四次握手

    图示过程如下:

    • 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
    • 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
    • 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
    • 接收到这个FIN的源发送端TCP对它进行确认。

    这样每个方向上都有一个FIN和ACK。

    6、一个例子(实践一下)

    说了这么多了,动手实践一下。下面编写一个简单的服务器、客户端(使用TCP)——服务器端一直监听本机的6666号端口,如果收到连接请求,将接收请求并接收客户端发来的消息;客户端与服务器端建立连接并发送一条消息。

    服务器端代码:

    #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) { int listenfd, connfd; struct sockaddr_in servaddr; 
    char buff[4096]; int n;
    if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){ printf("create socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } 
    memset(&servaddr, 0, sizeof(servaddr)); 
    servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(6666); 
    if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){ printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } 
    if( listen(listenfd, 10) == -1){ printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } 
    printf("======waiting for client's request======\n"); 
    while(1){ if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){ printf("accept socket error: %s(errno: %d)",strerror(errno),errno); continue; } n = recv(connfd, buff, MAXLINE, 0); buff[n] = '\0'; 
    printf("recv msg from client: %s\n", buff); close(connfd); } close(listenfd); }

    客户端代码:

    #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) { int sockfd, n; char recvline[4096], sendline[4096]; 
    struct sockaddr_in servaddr; if( argc != 2){ printf("usage: ./client <ipaddress>\n"); exit(0); } if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){ printf("create socket error: %s(errno: %d)\n", strerror(errno),errno); 
    exit(0); } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(6666); if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){ printf("inet_pton error for %s\n",argv[1]); exit(0); } 
    if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){ printf("connect error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } 
    printf("send msg to server: \n"); fgets(sendline, 4096, stdin); 
    if( send(sockfd, sendline, strlen(sendline), 0) < 0) { printf("send msg error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } close(sockfd); exit(0); }

    当然上面的代码很简单,也有很多缺点,这就只是简单的演示socket的基本函数使用。其实不管有多复杂的网络程序,都使用的这些基本函数。上面的服务器使用的是迭代模式的,即只有处理完一个客户端请求才会去处理下一个客户端的请求,这样的服务器处理能力是很弱的,现实中的服务器都需要有并发处理能力!为了需要并发处理,服务器需要fork()一个新的进程或者线程去处理请求等。

    7、动动手

    留下一个问题,欢迎大家回帖回答!!!是否熟悉Linux下网络编程?如熟悉,编写如下程序完成如下功能:

    服务器端:

    接收地址192.168.100.2的客户端信息,如信息为“Client Query”,则打印“Receive Query”

    客户端:

    向地址192.168.100.168的服务器端顺序发送信息“Client Query test”,“Cleint Query”,“Client Query Quit”,然后退出。

    题目中出现的ip地址可以根据实际情况定。

    ——本文只是介绍了简单的socket编程。

    更为复杂的需要自己继续深入。

    (unix domain socket)使用udp发送>=128K的消息会报ENOBUFS的错误(一个实际socket编程中遇到的问题,希望对你有帮助)

     

     

    作者:吴秦
    出处:http://www.cnblogs.com/skynet/
    本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名吴秦(包含链接).

    http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html

     

    展开全文
  • 【带你入门】java网络编程

    万次阅读 多人点赞 2018-02-18 12:10:41
    网络编程网络编程对于很多的初学者来说,都是很向往的一种编程技能,但是很多的初学者却因为很长一段时间无法进入网络编程的大门而放弃了对于该部分技术的学习。在 学习网络编程以前,很多初学者可能觉得网络编程是...
    
    

    ###网络编程

    网络编程对于很多的初学者来说,都是很向往的一种编程技能,但是很多的初学者却因为很长一段时间无法进入网络编程的大门而放弃了对于该部分技术的学习。

    在 学习网络编程以前,很多初学者可能觉得网络编程是比较复杂的系统工程,需要了解很多和网络相关的基础知识,其实这些都不是很必需的。首先来问一个问题:你 会打手机吗?很多人可能说肯定会啊,不就是按按电话号码,拨打电话嘛,很简单的事情啊!其实初学者如果入门网络编程的话也可以做到这么简单!

    网络编程就是在两个或两个以上的设备(例如计算机)之间传输数据。程序员所作的事情就是把数据发送到指定的位置,或者接收到指定的数据,这个就是狭义的网络编程范畴。在发送和接收数据时,大部分的程序设计语言都设计了专门的API实现这些功能,程序员只需要调用即可。所以,基础的网络编程可以和打电话一样简单。

    下面就开始Java语言的网络编程技术学习吧。
    ##1.1 网络概述

    网络编程技术是当前一种主流的编程技术,随着联网趋势的逐步增强以及网络应用程序的大量出现,所以在实际的开发中网络编程技术获得了大量的使用。本章中以浅 显的基础知识说明和实际的案例使广大初学者能够进入网络编程技术的大门,至于以后的实际修行就要阅读进阶的书籍以及进行大量的实际练习。
    ####1.1.1 计算机网络概述

    网络编程的实质就是两个(或多个)设备(例如计算机)之间的数据传输。

    按照计算机网络的定义,通过一定的物理设备将处于不同位置的计算机连接起来组成的网络,这个网络中包含的设备有:计算机、路由器、交换机等等。

    其实从软件编程的角度来说,对于物理设备的理解不需要很深刻,就像你打电话时不需要很熟悉通信网络的底层实现是一样的,但是当深入到网络编程的底层时,这些基础知识是必须要补的。

    路由器和交换机组成了核心的计算机网络,计算机只是这个网络上的节点以及控制等,通过光纤、网线等连接将设备连接起来,从而形成了一张巨大的计算机网络。

    网络最主要的优势在于共享:共享设备和数据,现在共享设备最常见的是打印机,一个公司一般一个打印机即可,共享数据就是将大量的数据存储在一组机器中,其它的计算机通过网络访问这些数据,例如网站、银行服务器等等。

    如果需要了解更多的网络硬件基础知识,可以阅读《计算机网络》教材,对于基础进行强化,这个在基础学习阶段不是必须的,但是如果想在网络编程领域有所造诣,则是一个必须的基本功。

    对于网络编程来说,最主要的是计算机和计算机之间的通信,这样首要的问题就是如何找到网络上的计算机呢?这就需要了解IP地址的概念。

    为了能够方便的识别网络上的每个设备,网络中的每个设备都会有一个唯一的数字标识,这个就是IP地址。在计算机网络中,现在命名IP地址的规定是IPv4协议,该协议规定每个IP地址由4个0-255之间的数字组成,例如10.0.120.34。每个接入网络的计算机都拥有唯一的IP地址,这个IP地址可能是固定的,例如网络上各种各样的服务器,也可以是动态的,例如使用ADSL拨号上网的宽带用户,无论以何种方式获得或是否是固定的,每个计算机在联网以后都拥有一个唯一的合法IP地址,就像每个手机号码一样。

    但是由于IP地址不容易记忆,所以为了方便记忆,有创造了另外一个概念——域名(Domain Name),例如sohu.com等。一个IP地址可以对应多个域名,一个域名只能对应一个IP地址。域名的概念可以类比手机中的通讯簿,由于手机号码不方便记忆,所以添加一个姓名标识号码,在实际拨打电话时可以选择该姓名,然后拨打即可。

    在网络中传输的数据,全部是以IP地址作为地址标识,所以在实际传输数据以前需要将域名转换为IP地址,实现这种功能的服务器称之为DNS服务器,也就是通俗的说法叫做域名解析。例如当用户在浏览器输入域名时,浏览器首先请求DNS服务器,将域名转换为IP地址,然后将转换后的IP地址反馈给浏览器,然后再进行实际的数据传输。

    当DNS服务器正常工作时,使用IP地址或域名都可以很方便的找到计算机网络中的某个设备,例如服务器计算机。当DNS不正常工作时,只能通过IP地址访问该设备。所以IP地址的使用要比域名通用一些。

    IP地址和域名很好的解决了在网络中找到一个计算机的问题,但是为了让一个计算机可以同时运行多个网络程序,就引入了另外一个概念——端口(port)。

    在介绍端口的概念以前,首先来看一个例子,一般一个公司前台会有一个电话,每个员工会有一个分机,这样如果需要找到这个员工的话,需要首先拨打前台总机,然后转该分机号即可。这样减少了公司的开销,也方便了每个员工。在该示例中前台总机的电话号码就相当于IP地址,而每个员工的分机号就相当于端口。

    有了端口的概念以后,在同一个计算机中每个程序对应唯一的端口,这样一个计算机上就可以通过端口区分发送给每个端口的数据了,换句话说,也就是一个计算机上可以并发运行多个网络程序,而不会在互相之间产生干扰。

    在硬件上规定,端口的号码必须位于0-65535之间,每个端口唯一的对应一个网络程序,一个网络程序可以使用多个端口。这样一个网络程序运行在一台计算上时,不管是客户端还是服务器,都是至少占用一个端口进行网络通讯。在接收数据时,首先发送给对应的计算机,然后计算机根据端口把数据转发给对应的程序。

    有了IP地址和端口的概念以后,在进行网络通讯交换时,就可以通过IP地址查找到该台计算机,然后通过端口标识这台计算机上的一个唯一的程序。这样就可以进行网络数据的交换了。

    但是,进行网络编程时,只有IP地址和端口的概念还是不够的,下面就介绍一下基础的网络编程相关的软件基础知识。
    ####1.1. 2 网络编程概述

    按照前面的介绍,网络编程就是两个或多个设备之间的数据交换,其实更具体的说,网络编程就是两个或多个程序之间的数据交换,和普通的单机程序相比,网络程序最大的不同就是需要交换数据的程序运行在不同的计算机上,这样就造成了数据交换的复杂。虽然通过IP地址和端口可以找到网络上运行的一个程序,但是如果需要进行网络编程,则还需要了解网络通讯的过程。

    网络通讯基于“请求-响应”模型。为了理解这个模型,先来看一个例子,经常看电视的人肯定见过审讯的场面吧,一般是这样的:

           警察:姓名
    
           嫌疑犯:XXX
    
           警察:性别
    
           嫌疑犯:男
    
           警察:年龄
    
           嫌疑犯:29
    
           ……
    

    在这个例子中,警察问一句,嫌疑犯回答一句,如果警察不问,则嫌疑犯保持沉默。这种一问一答的形式就是网络中的“请求-响应”模型。也就是通讯的一端发送数据,另外一端反馈数据,网络通讯都基于该模型。

    在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client)程序,简称客户端,而在第一次通讯中等待连接的程序被称作服务器端(Server)程序,简称服务器。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。

    由此,网络编程中的两种程序就分别是客户端和服务器端,例如QQ程序,每个QQ用户安装的都是QQ客户端程序,而QQ服务器端程序则运行在腾讯公司的机房中,为大量的QQ用户提供服务。这种网络编程的结构被称作客户端/服务器结构,也叫做Client/Server结构,简称C/S结构。

    使用C/S结 构的程序,在开发时需要分别开发客户端和服务器端,这种结构的优势在于由于客户端是专门开发的,所以根据需要实现各种效果,专业点说就是表现力丰富,而服 务器端也需要专门进行开发。但是这种结构也存在着很多不足,例如通用性差,几乎不能通用等,也就是说一种程序的客户端只能和对应的服务器端通讯,而不能和 其它服务器端通讯,在实际维护时,也需要维护专门的客户端和服务器端,维护的压力比较大。

    其实在运行很多程序时,没有必要使用专用的客户端,而需要使用通用的客户端,例如浏览器,使用浏览器作为客户端的结构被称作浏览器/服务器结构,也叫做Browser/Server结构,简称为B/S结构。

    使用B/S结构的程序,在开发时只需要开发服务器端即可,这种结构的优势在于开发的压力比较小,不需要维护客户端。但是这种结构也存在着很多不足,例如浏览器的限制比较大,表现力不强,无法进行系统级操作等。

    总之C/S结构和B/S结构是现在网络编程中常见的两种结构,B/S结构其实也就是一种特殊的C/S结构。

    另外简单的介绍一下P2P(Point to Point)程序,常见的如BT、电驴等。P2P程序是一种特殊的程序,应该一个P2P程序中既包含客户端程序,也包含服务器端程序,例如BT,使用客户端程序部分连接其它的种子(服务器端),而使用服务器端向其它的BT客户端传输数据。如果这个还不是很清楚,其实P2P程序和手机是一样的,当手机拨打电话时就是使用客户端的作用,而手机处于待机状态时,可以接收到其它用户拨打的电话则起的就是服务器端的功能,只是一般的手机不能同时使用拨打电话和接听电话的功能,而P2P程序实现了该功能。

    最后再介绍一个网络编程中最重要,也是最复杂的概念——协议(Protocol)。按照前面的介绍,网络编程就是运行在不同计算机中两个程序之间的数据交换。在实际进行数据交换时,为了让接收端理解该数据,计算机比较笨,什么都不懂的,那么就需要规定该数据的格式,这个数据的格式就是协议。

    如 果没有理解协议的概念,那么再举一个例子,记得有个电影叫《永不消逝的电波》,讲述的是地下党通过电台发送情报的故事,这里我们不探讨电影的剧情,而只关 心电台发送的数据。在实际发报时,需要首先将需要发送的内容转换为电报编码,然后将电报编码发送出去,而接收端接收的是电报编码,如果需要理解电报的内容 则需要根据密码本翻译出该电报的内容。这里的密码本就规定了一种数据格式,这种对于网络中传输的数据格式在网络编程中就被称作协议。

    那么如何来编写协议格式呢?答案是随意。只要按照这种协议格式能够生成唯一的编码,按照该编码可以唯一的解析出发送数据的内容即可。也正因为各个网络程序之间协议格式的不同,所以才导致了客户端程序都是专用的结构。

    在实际的网络程序编程中,最麻烦的内容不是数据的发送和接收,因为这个功能在几乎所有的程序语言中都提供了封装好的API进行调用,最麻烦的内容就是协议的设计以及协议的生产和解析,这个才是网络编程中最核心的内容。

    关于网络编程的基础知识,就介绍这里,深刻理解IP地址、端口和协议等概念,将会极大的有助于后续知识的学习。
    13.1.3 网络通讯方式

    在现有的网络中,网络通讯的方式主要有两种:

    • TCP(传输控制协议)方式

    • UDP(用户数据报协议)方式

    为 了方便理解这两种方式,还是先来看一个例子。大家使用手机时,向别人传递信息时有两种方式:拨打电话和发送短信。使用拨打电话的方式可以保证将信息传递给 别人,因为别人接听电话时本身就确认接收到了该信息。而发送短信的方式价格低廉,使用方便,但是接收人有可能接收不到。

    在网络通讯中,TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。而UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。

    这两种传输方式都是实际的网络编程中进行使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则都通过UDP方式进行传递,在一些程序中甚至结合使用这两种方式进行数据的传递。

    由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。

    关于网络编程的基础知识就介绍这么多,如果需要深入了解相关知识请阅读专门的计算机网络书籍,下面开始介绍Java语言中网络编程的相关技术。

    ##1.2 网络编程技术

    前面介绍了网络编程的相关基础知识,初步建立了网络编程的概念,但是实际学习网络编程还必须使用某种程序设计语言进行代码实现,下面就介绍一下网络编程的代码实现。
    ####1.2.1 网络编程步骤

    按照前面的基础知识介绍,无论使用TCP方式还是UDP方式进行网络通讯,网络编程都是由客户端和服务器端组成。当然,B/S结构的编程中只需要实现服务器端即可。所以,下面介绍网络编程的步骤时,均以C/S结构为基础进行介绍。

    **说明:**这里的步骤实现和语言无关,也就是说,这个步骤适用于各种语言实现,不局限于Java语言。
    #####1.2.1.1 客户端网络编程步骤

    客户端(Client)是指网络编程中首先发起连接的程序,客户端一般实现程序界面和基本逻辑实现,在进行实际的客户端编程时,无论客户端复杂还是简单,以及客户端实现的方式,客户端的编程主要由三个步骤实现:

    1、 建立网络连接

    客户端网络编程的第一步都是建立网络连接。在建立网络连接时需要指定连接到的服务器的IP地址和端口号,建立完成以后,会形成一条虚拟的连接,后续的操作就可以通过该连接实现数据交换了。

    2、 交换数据

    连接建立以后,就可以通过这个连接交换数据了。交换数据严格按照请求响应模型进行,由客户端发送一个请求数据到服务器,服务器反馈一个响应数据给客户端,如果客户端不发送请求则服务器端就不响应。

    根据逻辑需要,可以多次交换数据,但是还是必须遵循请求响应模型。

    3、 关闭网络连接

    在数据交换完成以后,关闭网络连接,释放程序占用的端口、内存等系统资源,结束网络编程。

    最基本的步骤一般都是这三个步骤,在实际实现时,步骤2会出现重复,在进行代码组织时,由于网络编程是比较耗时的操作,所以一般开启专门的现场进行网络通讯。
    #####1.2.1.2 服务器端网络编程步骤

    服务器端(Server)是指在网络编程中被动等待连接的程序,服务器端一般实现程序的核心逻辑以及数据存储等核心功能。服务器端的编程步骤和客户端不同,是由四个步骤实现,依次是:

    1、 监听端口

    服务器端属于被动等待连接,所以服务器端启动以后,不需要发起连接,而只需要监听本地计算机的某个固定端口即可。

    这个端口就是服务器端开放给客户端的端口,服务器端程序运行的本地计算机的IP地址就是服务器端程序的IP地址。

    2、 获得连接

    当客户端连接到服务器端时,服务器端就可以获得一个连接,这个连接包含客户端的信息,例如客户端IP地址等等,服务器端和客户端也通过该连接进行数据交换。

    一般在服务器端编程中,当获得连接时,需要开启专门的线程处理该连接,每个连接都由独立的线程实现。

    3、 交换数据

    服务器端通过获得的连接进行数据交换。服务器端的数据交换步骤是首先接收客户端发送过来的数据,然后进行逻辑处理,再把处理以后的结果数据发送给客户端。简单来说,就是先接收再发送,这个和客户端的数据交换数序不同。

    其实,服务器端获得的连接和客户端连接是一样的,只是数据交换的步骤不同。

    当然,服务器端的数据交换也是可以多次进行的。

    在数据交换完成以后,关闭和客户端的连接。

    4、 关闭连接

    当服务器程序关闭时,需要关闭服务器端,通过关闭服务器端使得服务器监听的端口以及占用的内存可以释放出来,实现了连接的关闭。

    其实服务器端编程的模型和呼叫中心的实现是类似的,例如移动的客服电话10086就是典型的呼叫中心,当一个用户拨打10086时,转接给一个专门的客服人员,由该客服实现和该用户的问题解决,当另外一个用户拨打10086时,则转接给另一个客服,实现问题解决,依次类推。

    在服务器端编程时,10086这个电话号码就类似于服务器端的端口号码,每个用户就相当于一个客户端程序,每个客服人员就相当于服务器端启动的专门和客户端连接的线程,每个线程都是独立进行交互的。

    这就是服务器端编程的模型,只是TCP方式是需要建立连接的,对于服务器端的压力比较大,而UDP是不需要建立连接的,对于服务器端的压力比较小罢了。
    #####1.2.1.3 小结

    总之,无论使用任何语言,任何方式进行基础的网络编程,都必须遵循固定的步骤进行操作,在熟悉了这些步骤以后,可以根据需要进行逻辑上的处理,但是还是必须遵循固定的步骤进行。

    其实,基础的网络编程本身不难,也不需要很多的基础网络知识,只是由于编程的基础功能都已经由API实现,而且需要按照固定的步骤进行,所以在入门时有一定的门槛,希望下面的内容能够将你快速的带入网络编程技术的大门。
    ####1.2.2 Java网络编程技术

    Java语言是在网络环境下诞生的,所以Java语言虽然不能说是对于网络编程的支持最好的语言,但是必须说是一种对于网络编程提供良好支持的语言,使用Java语言进行网络编程将是一件比较轻松的工作。

    和网络编程有关的基本API位于java.net包中,该包中包含了基本的网络编程实现,该包是网络编程的基础。该包中既包含基础的网络编程类,也包含封装后的专门处理WEB相关的处理类。在本章中,将只介绍基础的网络编程类。

    首先来介绍一个基础的网络类——InetAddress类。该类的功能是代表一个IP地址,并且将IP地址和域名相关的操作方法包含在该类的内部。

    关于该类的使用,下面通过一个基础的代码示例演示该类的使用,代码如下:

     package inetaddressdemo;
    
    import java.net.*;
    
    /**
    
     * 演示InetAddress类的基本使用
    
     */
    
    public class InetAddressDemo {
    
             public static void main(String[] args) {
    
                       try{
    
                                //使用域名创建对象
    
                                InetAddress inet1 = InetAddress.getByName("www.163.com");
    
                                System.out.println(inet1);
    
                                //使用IP创建对象
    
                                InetAddress inet2 = InetAddress.getByName("127.0.0.1");
    
                                System.out.println(inet2);
    
                                //获得本机地址对象
    
                                InetAddress inet3 = InetAddress.getLocalHost();
    
                                System.out.println(inet3);
    
                                //获得对象中存储的域名
    
                                String host = inet3.getHostName();
    
                                System.out.println("域名:" + host);
    
                                //获得对象中存储的IP
    
                                String ip = inet3.getHostAddress();
    
                                System.out.println("IP:" + ip);
    
                       }catch(Exception e){}
    
             }
    
    }
    

    在该示例代码中,演示了InetAddress类的基本使用,并使用了该类中的几个常用方法,该代码的执行结果是:

    www.163.com/220.181.28.50
    
    /127.0.0.1
    
    chen/192.168.1.100
    
    域名:chen
    
    IP:192.168.1.100
    

    **说明:**由于该代码中包含一个互联网的网址,所以运行该程序时需要联网,否则将产生异常。

    在后续的使用中,经常包含需要使用InetAddress对象代表IP地址的构造方法,当然,该类的使用不是必须的,也可以使用字符串来代表IP地址进行实现。
    ####1.2.3 TCP编程

    按照前面的介绍,网络通讯的方式有TCP和UDP两种,其中TCP方式的网络通讯是指在通讯的过程中保持连接,有点类似于打电话,只需要拨打一次号码(建立一次网络连接),就可以多次通话(多次传输数据)。这样方式在实际的网络编程中,由于传输可靠,类似于打电话,如果甲给乙打电话,乙说没有听清楚让甲重复一遍,直到乙听清楚为止,实际的网络传输也是这样,如果发送的一方发送的数据接收方觉得有问题,则网络底层会自动要求发送方重发,直到接收方收到为止。

    在Java语言中,对于TCP方式的网络编程提供了良好的支持,在实际实现时,以java.net.Socket类代表客户端连接,以java.net.ServerSocket类代表服务器端连接。在进行网络编程时,底层网络通讯的细节已经实现了比较高的封装,所以在程序员实际编程时,只需要指定IP地址和端口号码就可以建立连接了。正是由于这种高度的封装,一方面简化了Java语言网络编程的难度,另外也使得使用Java语言进行网络编程时无法深入到网络的底层,所以使用Java语言进行网络底层系统编程很困难,具体点说,Java语言无法实现底层的网络嗅探以及获得IP包结构等信息。但是由于Java语言的网络编程比较简单,所以还是获得了广泛的使用。

    在使用TCP方式进行网络编程时,需要按照前面介绍的网络编程的步骤进行,下面分别介绍一下在Java语言中客户端和服务器端的实现步骤。

    在客户端网络编程中,首先需要建立连接,在Java API中以java.net.Socket类的对象代表网络连接,所以建立客户端网络连接,也就是创建Socket类型的对象,该对象代表网络连接,示例如下:

         Socket socket1 = new Socket(“192.168.1.103”,10000);
    
         Socket socket2 = new Socket(“www.sohu.com”,80);
    

    上面的代码中,socket1实现的是连接到IP地址是192.168.1.103的计算机的10000号端口,而socket2实现的是连接到域名是www.sohu.com的计算机的80号端口,至于底层网络如何实现建立连接,对于程序员来说是完全透明的。如果建立连接时,本机网络不通,或服务器端程序未开启,则会抛出异常。

    连接一旦建立,则完成了客户端编程的第一步,紧接着的步骤就是按照“请求-响应”模型进行网络数据交换,在Java语言中,数据传输功能由Java IO实现,也就是说只需要从连接中获得输入流和输出流即可,然后将需要发送的数据写入连接对象的输出流中,在发送完成以后从输入流中读取数据即可。示例代码如下:

         OutputStream os = socket1.getOutputStream(); //获得输出流
    
         InputStream is = socket1.getInputStream();     //获得输入流
    

    上面的代码中,分别从socket1这个连接对象获得了输出流和输入流对象,在整个网络编程中,后续的数据交换就变成了IO操作,也就是遵循“请求-响应”模型的规定,先向输出流中写入数据,这些数据会被系统发送出去,然后在从输入流中读取服务器端的反馈信息,这样就完成了一次数据交换过程,当然这个数据交换过程可以多次进行。

    这里获得的只是最基本的输出流和输入流对象,还可以根据前面学习到的IO知识,使用流的嵌套将这些获得到的基本流对象转换成需要的装饰流对象,从而方便数据的操作。

    最后当数据交换完成以后,关闭网络连接,释放网络连接占用的系统端口和内存等资源,完成网络操作,示例代码如下:

          socket1.close();
    

    这就是最基本的网络编程功能介绍。下面是一个简单的网络客户端程序示例,该程序的作用是向服务器端发送一个字符串“Hello”,并将服务器端的反馈显示到控制台,数据交换只进行一次,当数据交换进行完成以后关闭网络连接,程序结束。实现的代码如下:

    package tcp;
    
    import java.io.*;
    
    import java.net.*;
    
    /**
    
     * 简单的Socket客户端
    
     * 功能为:发送字符串“Hello”到服务器端,并打印出服务器端的反馈
    
     */
    
    public class SimpleSocketClient {
    
             public static void main(String[] args) {
    
                       Socket socket = null;
    
                       InputStream is = null;
    
                       OutputStream os = null;
    
                       //服务器端IP地址
    
                       String serverIP = "127.0.0.1";
    
                       //服务器端端口号
    
                       int port = 10000;
    
                       //发送内容
    
                       String data = "Hello";
    
                       try {
    
                                //建立连接
    
                                socket = new Socket(serverIP,port);
    
                                //发送数据
    
                                os = socket.getOutputStream();
    
                                os.write(data.getBytes());
    
                                //接收数据
    
                                is = socket.getInputStream();
    
                                byte[] b = new byte[1024];
    
                                int n = is.read(b);
    
                                //输出反馈数据
    
                                System.out.println("服务器反馈:" + new String(b,0,n));
    
                       } catch (Exception e) {
    
                                e.printStackTrace(); //打印异常信息
    
                       }finally{
    
                                try {
    
                                         //关闭流和连接
    
                                         is.close();
    
                                         os.close();
    
                                         socket.close();
    
                                } catch (Exception e2) {}
    
                       }
    
             }
    
    }
    

    在该示例代码中建立了一个连接到IP地址为127.0.0.1,端口号码为10000的TCP类型的网络连接,然后获得连接的输出流对象,将需要发送的字符串“Hello”转换为byte数组写入到输出流中,由系统自动完成将输出流中的数据发送出去,如果需要强制发送,可以调用输出流对象中的flush方法实现。在数据发送出去以后,从连接对象的输入流中读取服务器端的反馈信息,读取时可以使用IO中的各种读取方法进行读取,这里使用最简单的方法进行读取,从输入流中读取到的内容就是服务器端的反馈,并将读取到的内容在客户端的控制台进行输出,最后依次关闭打开的流对象和网络连接对象。

    这是一个简单的功能示例,在该示例中演示了TCP类型的网络客户端基本方法的使用,该代码只起演示目的,还无法达到实用的级别。

    如果需要在控制台下面编译和运行该代码,需要首先在控制台下切换到源代码所在的目录,然后依次输入编译和运行命令:

         javac –d . SimpleSocketClient.java
    
             java tcp.SimpleSocketClient
    

    和下面将要介绍的SimpleSocketServer服务器端组合运行时,程序的输出结果为:

         服务器反馈:Hello
    

    介绍完一个简单的客户端编程的示例,下面接着介绍一下TCP类型的服务器端的编写。首先需要说明的是,客户端的步骤和服务器端的编写步骤不同,所以在学习服务器端编程时注意不要和客户端混淆起来。

    在服务器端程序编程中,由于服务器端实现的是被动等待连接,所以服务器端编程的第一个步骤是监听端口,也就是监听是否有客户端连接到达。实现服务器端监听的代码为:

        ServerSocket ss = new ServerSocket(10000);
    

    该代码实现的功能是监听当前计算机的10000号端口,如果在执行该代码时,10000号端口已经被别的程序占用,那么将抛出异常。否则将实现监听。

    服务器端编程的第二个步骤是获得连接。该步骤的作用是当有客户端连接到达时,建立一个和客户端连接对应的Socket连 接对象,从而释放客户端连接对于服务器端端口的占用。实现功能就像公司的前台一样,当一个客户到达公司时,会告诉前台我找某某某,然后前台就通知某某某, 然后就可以继续接待其它客户了。通过获得连接,使得客户端的连接在服务器端获得了保持,另外使得服务器端的端口释放出来,可以继续等待其它的客户端连接。 实现获得连接的代码是:

         Socket socket = ss.accept();
    

    该代码实现的功能是获得当前连接到服务器端的客户端连接。需要说明的是accept和前面IO部分介绍的read方法一样,都是一个阻塞方法,也就是当无连接时,该方法将阻塞程序的执行,直到连接到达时才执行该行代码。另外获得的连接会在服务器端的该端口注册,这样以后就可以通过在服务器端的注册信息直接通信,而注册以后服务器端的端口就被释放出来,又可以继续接受其它的连接了。

    连接获得以后,后续的编程就和客户端的网络编程类似了,这里获得的Socket类型的连接就和客户端的网络连接一样了,只是服务器端需要首先读取发送过来的数据,然后进行逻辑处理以后再发送给客户端,也就是交换数据的顺序和客户端交换数据的步骤刚好相反。这部分的内容和客户端很类似,所以就不重复了,如果还不熟悉,可以参看下面的示例代码。

    最后,在服务器端通信完成以后,关闭服务器端连接。实现的代码为:

        ss.close();
    

    这就是基本的TCP类型的服务器端编程步骤。下面以一个简单的echo服务实现为例子,介绍综合使用示例。echo的意思就是“回声”,echo服务器端实现的功能就是将客户端发送的内容再原封不动的反馈给客户端。实现的代码如下:

    package tcp;
    
    import java.io.*;
    
    import java.net.*;
    
    /**
    
     * echo服务器
    
     * 功能:将客户端发送的内容反馈给客户端
    
     */
    
    public class SimpleSocketServer {
    
             public static void main(String[] args) {
    
                       ServerSocket serverSocket = null;
    
                       Socket socket = null;
    
                       OutputStream os = null;
    
                       InputStream is = null;
    
                       //监听端口号
    
                       int port = 10000;
    
                       try {
    
                                //建立连接
    
                                serverSocket = new ServerSocket(port);
    
                                //获得连接
    
                                socket = serverSocket.accept();
    
                                //接收客户端发送内容
    
                                is = socket.getInputStream();
    
                                byte[] b = new byte[1024];
    
                                int n = is.read(b);
    
                                //输出
    
                                System.out.println("客户端发送内容为:" + new String(b,0,n));
    
                                //向客户端发送反馈内容
    
                                os = socket.getOutputStream();
    
                                os.write(b, 0, n);
    
                       } catch (Exception e) {
    
                                e.printStackTrace();
    
                       }finally{
    
                                try{
    
                                         //关闭流和连接
    
                                         os.close();
    
                                         is.close();
    
                                         socket.close();
    
                                         serverSocket.close();
    
                                }catch(Exception e){}
    
                       }
    
             }
    
    }
    

    在该示例代码中建立了一个监听当前计算机10000号端口的服务器端Socket连接,然后获得客户端发送过来的连接,如果有连接到达时,读取连接中发送过来的内容,并将发送的内容在控制台进行输出,输出完成以后将客户端发送的内容再反馈给客户端。最后关闭流和连接对象,结束程序。

    在控制台下面编译和运行该程序的命令和客户端部分的类似。

    这样,就以一个很简单的示例演示了TCP类型的网络编程在Java语言中的基本实现,这个示例只是演示了网络编程的基本步骤以及各个功能方法的基本使用,只是为网络编程打下了一个基础,下面将就几个问题来深入介绍网络编程深层次的一些知识。
    为了一步一步的掌握网络编程,下面再研究网络编程中的两个基本问题,通过解决这两个问题将对网络编程的认识深入一层。

    1、如何复用Socket连接?

    在前面的示例中,客户端中建立了一次连接,只发送一次数据就关闭了,这就相当于拨打电话时,电话打通了只对话一次就关闭了,其实更加常用的应该是拨通一次电话以后多次对话,这就是复用客户端连接。

    那 么如何实现建立一次连接,进行多次数据交换呢?其实很简单,建立连接以后,将数据交换的逻辑写到一个循环中就可以了。这样只要循环不结束则连接就不会被关 闭。按照这种思路,可以改造一下上面的代码,让该程序可以在建立连接一次以后,发送三次数据,当然这里的次数也可以是多次,示例代码如下:

    package tcp;
    
    import java.io.*;
    
    import java.net.*;
    
    /**
    
     * 复用连接的Socket客户端
    
     * 功能为:发送字符串“Hello”到服务器端,并打印出服务器端的反馈
    
     */
    
    public class MulSocketClient {
    
             public static void main(String[] args) {
    
                       Socket socket = null;
    
                       InputStream is = null;
    
                       OutputStream os = null;
    
                       //服务器端IP地址
    
                       String serverIP = "127.0.0.1";
    
                       //服务器端端口号
    
                       int port = 10000;
    
                       //发送内容
    
                       String data[] ={"First","Second","Third"};
    
                       try {
    
                                //建立连接
    
                                socket = new Socket(serverIP,port);
    
                                //初始化流
    
                                os = socket.getOutputStream();
    
                                is = socket.getInputStream();
    
                                byte[] b = new byte[1024];
    
                                for(int i = 0;i < data.length;i++){
    
                                         //发送数据
    
                                         os.write(data[i].getBytes());
    
                                         //接收数据
    
                                         int n = is.read(b);
    
                                         //输出反馈数据
    
                                         System.out.println("服务器反馈:" + new String(b,0,n));
    
                                }
    
                       } catch (Exception e) {
    
                                e.printStackTrace(); //打印异常信息
    
                       }finally{
    
                                try {
    
                                         //关闭流和连接
    
                                         is.close();
    
                                         os.close();
    
                                         socket.close();
    
                                } catch (Exception e2) {}
    
                       }
    
             }
    
    }
    

    该示例程序和前面的代码相比,将数据交换部分的逻辑写在一个for循环的内容,这样就可以建立一次连接,依次将data数组中的数据按照顺序发送给服务器端了。

    如果还是使用前面示例代码中的服务器端程序运行该程序,则该程序的结果是:

    java.net.SocketException: Software caused connection abort: recv failed
    
                                         at java.net.SocketInputStream.socketRead0(Native Method)
    
                                         at java.net.SocketInputStream.read(SocketInputStream.java:129)
    
                                         at java.net.SocketInputStream.read(SocketInputStream.java:90)
    
                                         at tcp.MulSocketClient.main(MulSocketClient.java:30)
    
    服务器反馈:First
    

    显然,客户端在实际运行时出现了异常,出现异常的原因是什么呢?如果仔细阅读前面的代码,应该还记得前面示例代码中的服务器端是对话一次数据以后就关闭了连接,如果服务器端程序关闭了,客户端继续发送数据肯定会出现异常,这就是出现该问题的原因。

    按照客户端实现的逻辑,也可以复用服务器端的连接,实现的原理也是将服务器端的数据交换逻辑写在循环中即可,按照该种思路改造以后的服务器端代码为:

    package tcp;
    
    import java.io.*;
    
    import java.net.*;
    
    /**
    
     * 复用连接的echo服务器
    
     * 功能:将客户端发送的内容反馈给客户端
    
     */
    
    public class MulSocketServer {
    
             public static void main(String[] args) {
    
                       ServerSocket serverSocket = null;
    
                       Socket socket = null;
    
                       OutputStream os = null;
    
                       InputStream is = null;
    
                       //监听端口号
    
                       int port = 10000;
    
                       try {
    
                                //建立连接
    
                                serverSocket = new ServerSocket(port);
    
                                System.out.println("服务器已启动:");
    
                                //获得连接
    
                                socket = serverSocket.accept();
    
                                //初始化流
    
                                is = socket.getInputStream();
    
                                os = socket.getOutputStream();
    
                                byte[] b = new byte[1024];
    
                                for(int i = 0;i < 3;i++){
    
                                         int n = is.read(b);
    
                                         //输出
    
                                         System.out.println("客户端发送内容为:" + new String(b,0,n));
    
                                         //向客户端发送反馈内容
    
                                         os.write(b, 0, n);
    
                                }
    
                       } catch (Exception e) {
    
                                e.printStackTrace();
    
                       }finally{
    
                                try{
    
                                         //关闭流和连接
    
                                         os.close();
    
                                         is.close();
    
                                         socket.close();
    
                                         serverSocket.close();
    
                                }catch(Exception e){}
    
                       }
    
             }
    
    }
    

    在该示例代码中,也将数据发送和接收的逻辑写在了一个for循环内部,只是在实现时硬性的将循环次数规定成了3次,这样代码虽然比较简单,但是通用性比较差。

    以该服务器端代码实现为基础运行前面的客户端程序时,客户端的输出为:

    服务器反馈:First
    
    服务器反馈:Second
    
    服务器反馈:Third
    

    服务器端程序的输出结果为:

    服务器已启动:
    
    客户端发送内容为:First
    
    客户端发送内容为:Second
    
    客户端发送内容为:Third
    

    在该程序中,比较明显的体现出了“请求-响应”模型,也就是在客户端发起连接以后,首先发送字符串“First”给服务器端,服务器端输出客户端发送的内容“First”,然后将客户端发送的内容再反馈给客户端,这样客户端也输出服务器反馈“First”,这样就完成了客户端和服务器端的一次对话,紧接着客户端发送“Second”给服务器端,服务端输出“Second”,然后将“Second”再反馈给客户端,客户端再输出“Second”,从而完成第二次会话,第三次会话的过程和这个一样。在这个过程中,每次都是客户端程序首先发送数据给服务器端,服务器接收数据以后,将结果反馈给客户端,客户端接收到服务器端的反馈,从而完成一次通讯过程。

    在该示例中,虽然解决了多次发送的问题,但是客户端和服务器端的次数控制还不够灵活,如果客户端的次数不固定怎么办呢?是否可以使用某个特殊的字符串,例如quit,表示客户端退出呢,这就涉及到网络协议的内容了,会在后续的网络应用示例部分详细介绍。下面开始介绍另外一个网络编程的突出问题。

    2、如何使服务器端支持多个客户端同时工作?

    前面介绍的服务器端程序,只是实现了概念上的服务器端,离实际的服务器端程序结构距离还很遥远,如果需要让服务器端能够实际使用,那么最需要解决的问题就是——如何支持多个客户端同时工作。

    一个服务器端一般都需要同时为多个客户端提供通讯,如果需要同时支持多个客户端,则必须使用前面介绍的线程的概念。简单来说,也就是当服务器端接收到一个连接时,启动一个专门的线程处理和该客户端的通讯。

    按照这个思路改写的服务端示例程序将由两个部分组成,MulThreadSocketServer类实现服务器端控制,实现接收客户端连接,然后开启专门的逻辑线程处理该连接,LogicThread类实现对于一个客户端连接的逻辑处理,将处理的逻辑放置在该类的run方法中。该示例的代码实现为:

    package tcp;
    
    import java.net.ServerSocket;
    
    import java.net.Socket;
    
    /**
    
     * 支持多客户端的服务器端实现
    
     */
    
    public class MulThreadSocketServer {
    
             public static void main(String[] args) {
    
                       ServerSocket serverSocket = null;
    
                       Socket socket = null;
    
                       //监听端口号
    
                       int port = 10000;
    
                       try {
    
                                //建立连接
    
                                serverSocket = new ServerSocket(port);
    
                                System.out.println("服务器已启动:");
    
                                while(true){
    
                                         //获得连接
    
                                         socket = serverSocket.accept();
    
                                         //启动线程
    
                                         new LogicThread(socket);
    
                                }
    
                       } catch (Exception e) {
    
                                e.printStackTrace();
    
                       }finally{
    
                                try{
    
                                         //关闭连接
    
                                         serverSocket.close();
    
                                }catch(Exception e){}
    
                       }
    
             }
    
    }
    

    在该示例代码中,实现了一个while形式的死循环,由于accept方法是阻塞方法,所以当客户端连接未到达时,将阻塞该程序的执行,当客户端到达时接收该连接,并启动一个新的LogicThread线程处理该连接,然后按照循环的执行流程,继续等待下一个客户端连接。这样当任何一个客户端连接到达时,都开启一个专门的线程处理,通过多个线程支持多个客户端同时处理。

    下面再看一下LogicThread线程类的源代码实现:

    package tcp;
    
    import java.io.*;
    
    import java.net.*;
    
    /**
    
     * 服务器端逻辑线程
    
     */
    
    public class LogicThread extends Thread {
    
             Socket socket;
    
             InputStream is;
    
             OutputStream os;
    
             public LogicThread(Socket socket){
    
                       this.socket = socket;
    
                       start(); //启动线程
    
             }
    
            
    
             public void run(){
    
                       byte[] b = new byte[1024];
    
                       try{
    
                                //初始化流
    
                                os = socket.getOutputStream();
    
                                is = socket.getInputStream();
    
                                for(int i = 0;i < 3;i++){
    
                                         //读取数据
    
                                         int n = is.read(b);
    
                                         //逻辑处理
    
                                         byte[] response = logic(b,0,n);
    
                                         //反馈数据
    
                                         os.write(response);
    
                                }
    
                       }catch(Exception e){
    
                                e.printStackTrace();
    
                       }finally{
    
                                close();
    
                       }
    
             }
    
            
    
             /**
    
              * 关闭流和连接
    
              */
    
             private void close(){
    
                       try{
    
                                //关闭流和连接
    
                                os.close();
    
                                is.close();
    
                                socket.close();
    
                       }catch(Exception e){}
    
             }
    
            
    
             /**
    
              * 逻辑处理方法,实现echo逻辑
    
              * @param b 客户端发送数据缓冲区
    
              * @param off 起始下标
    
              * @param len 有效数据长度
    
              * @return
    
              */
    
             private byte[] logic(byte[] b,int off,int len){
    
                       byte[] response = new byte[len];
    
                       //将有效数据拷贝到数组response中
    
                       System.arraycopy(b, 0, response, 0, len);
    
                       return response;
    
             }
    
    }
    

    在该示例代码中,每次使用一个连接对象构造该线程,该连接对象就是该线程需要处理的连接,在线程构造完成以后,该线程就被启动起来了,然后在run方法内部对客户端连接进行处理,数据交换的逻辑和前面的示例代码一致,只是这里将接收到客户端发送过来的数据并进行处理的逻辑封装成了logic方法,按照前面介绍的IO编程的内容,客户端发送过来的内容存储在数组b的起始下标为0,长度为n个中,这些数据是客户端发送过来的有效数据,将有效的数据传递给logic方法,logic方法实现的是echo服务的逻辑,也就是将客户端发送的有效数据形成以后新的response数组,并作为返回值反馈。

    在线程中将logic方法的返回值反馈给客户端,这样就完成了服务器端的逻辑处理模拟,其他的实现和前面的介绍类似,这里就不在重复了。

    这里的示例还只是基础的服务器端实现,在实际的服务器端实现中,由于硬件和端口数的限制,所以不能无限制的创建线程对象,而且频繁的创建线程对象效率也比较低,所以程序中都实现了线程池来提高程序的执行效率。

    这里简单介绍一下线程池的概念,线程池(Thread pool)是池技术的一种,就是在程序启动时首先把需要个数的线程对象创建好,例如创建5000个线程对象,然后当客户端连接到达时从池中取出一个已经创建完成的线程对象使用即可。当客户端连接关闭以后,将该线程对象重新放入到线程池中供其它的客户端重复使用,这样可以提高程序的执行速度,优化程序对于内存的占用等。

    关于基础的TCP方式的网络编程就介绍这么多,下面介绍UDP方式的网络编程在Java语言中的实现。

    网络通讯的方式除了TCP方式以外,还有一种实现的方式就是UDP方式。UDP(User Datagram Protocol),中文意思是用户数据报协议,方式类似于发短信息,是一种物美价廉的通讯方式,使用该种方式无需建立专用的虚拟连接,由于无需建立专用的连接,所以对于服务器的压力要比TCP小很多,所以也是一种常见的网络编程方式。但是使用该种方式最大的不足是传输不可靠,当然也不是说经常丢失,就像大家发短信息一样,理论上存在收不到的可能,这种可能性可能是1%,反正比较小,但是由于这种可能的存在,所以平时我们都觉得重要的事情还是打个电话吧(类似TCP方式),一般的事情才发短信息(类似UDP方式)。网络编程中也是这样,必须要求可靠传输的信息一般使用TCP方式实现,一般的数据才使用UDP方式实现。

    UDP方式的网络编程也在Java语言中获得了良好的支持,由于其在传输数据的过程中不需要建立专用的连接等特点,所以在Java API中设计的实现结构和TCP方式不太一样。当然,需要使用的类还是包含在java.net包中。

    在Java API中,实现UDP方式的编程,包含客户端网络编程和服务器端网络编程,主要由两个类实现,分别是:

    DatagramSocket

    DatagramSocket类实现“网络连接”,包括客户端网络连接和服务器端网络连接。虽然UDP方式的网络通讯不需要建立专用的网络连接,但是毕竟还是需要发送和接收数据,DatagramSocket实现的就是发送数据时的发射器,以及接收数据时的监听器的角色。类比于TCP中的网络连接,该类既可以用于实现客户端连接,也可以用于实现服务器端连接。

    DatagramPacket

    DatagramPacket类实现对于网络中传输的数据封装,也就是说,该类的对象代表网络中交换的数据。在UDP方式的网络编程中,无论是需要发送的数据还是需要接收的数据,都必须被处理成DatagramPacket类型的对象,该对象中包含发送到的地址、发送到的端口号以及发送的内容等。其实DatagramPacket类的作用类似于现实中的信件,在信件中包含信件发送到的地址以及接收人,还有发送的内容等,邮局只需要按照地址传递即可。在接收数据时,接收到的数据也必须被处理成DatagramPacket类型的对象,在该对象中包含发送方的地址、端口号等信息,也包含数据的内容。和TCP方式的网络传输相比,IO编程在UDP方式的网络编程中变得不是必须的内容,结构也要比TCP方式的网络编程简单一些。

    下面介绍一下UDP方式的网络编程中,客户端和服务器端的实现步骤,以及通过基础的示例演示UDP方式的网络编程在Java语言中的实现方式。

    UDP方式的网络编程,编程的步骤和TCP方式类似,只是使用的类和方法存在比较大的区别,下面首先介绍一下UDP方式的网络编程客户端实现过程。

    UDP客户端编程涉及的步骤也是4个部分:建立连接、发送数据、接收数据和关闭连接。

    首先介绍UDP方式的网络编程中建立连接的实现。其中UDP方式的建立连接和TCP方式不同,只需要建立一个连接对象即可,不需要指定服务器的IP和端口号码。实现的代码为:

         DatagramSocket ds = new DatagramSocket();
    

    这样就建立了一个客户端连接,该客户端连接使用系统随机分配的一个本地计算机的未用端口号。在该连接中,不指定服务器端的IP和端口,所以UDP方式的网络连接更像一个发射器,而不是一个具体的连接。

    当然,可以通过制定连接使用的端口号来创建客户端连接。

                       DatagramSocket ds = new DatagramSocket(5000);
    

    这样就是使用本地计算机的5000号端口建立了一个连接。一般在建立客户端连接时没有必要指定端口号码。

    接着,介绍一下UDP客户端编程中发送数据的实现。在UDP方式的网络编程中,IO技术不是必须的,在发送数据时,需要将需要发送的数据内容首先转换为byte数组,然后将数据内容、服务器IP和服务器端口号一起构造成一个DatagramPacket类型的对象,这样数据的准备就完成了,发送时调用网络连接对象中的send方法发送该对象即可。例如将字符串“Hello”发送到IP是127.0.0.1,端口号是10001的服务器,则实现发送数据的代码如下:

          String s = “Hello”;
    
                       String host = “127.0.0.1”;
    
                       int port = 10001;
    
                      //将发送的内容转换为byte数组
    
                       byte[] b = s.getBytes();
    
                       //将服务器IP转换为InetAddress对象
    
                       InetAddress server = InetAddress.getByName(host);
    
                       //构造发送的数据包对象
    
                       DatagramPacket sendDp = new DatagramPacket(b,b.length,server,port);
    
                       //发送数据
    
                       ds.send(sendDp);
    

    在该示例代码中,不管发送的数据内容是什么,都需要转换为byte数组,然后将服务器端的IP地址构造成InetAddress类型的对象,在准备完成以后,将这些信息构造成一个DatagramPacket类型的对象,在UDP编程中,发送的数据内容、服务器端的IP和端口号,都包含在DatagramPacket对象中。在准备完成以后,调用连接对象ds的send方法把DatagramPacket对象发送出去即可。

    按照UDP协议的约定,在进行数据传输时,系统只是尽全力传输数据,但是并不保证数据一定被正确传输,如果数据在传输过程中丢失,那就丢失了。

    UDP方式在进行网络通讯时,也遵循“请求-响应”模型,在发送数据完成以后,就可以接收服务器端的反馈数据了。

    下面介绍一下UDP客户端编程中接收数据的实现。当数据发送出去以后,就可以接收服务器端的反馈信息了。接收数据在Java语言中的实现是这样的:首先构造一个数据缓冲数组,该数组用于存储接收的服务器端反馈数据,该数组的长度必须大于或等于服务器端反馈的实际有效数据的长度。然后以该缓冲数组为基础构造一个DatagramPacket数据包对象,最后调用连接对象的receive方法接收数据即可。接收到的服务器端反馈数据存储在DatagramPacket类型的对象内部。实现接收数据以及显示服务器端反馈内容的示例代码如下:

         //构造缓冲数组
    
                       byte[] data = new byte[1024];
    
                       //构造数据包对象
    
                       DatagramPacket received = new DatagramPacket(data,data.length);
    
                       //接收数据
    
                       ds.receive(receiveDp);
    
                       //输出数据内容
    
                       byte[] b = receiveDp.getData(); //获得缓冲数组
    
                       int len = receiveDp.getLength(); //获得有效数据长度
    
                       String s = new String(b,0,len);
    
                       System.out.println(s);
    

    在该代码中,首先构造缓冲数组data,这里设置的长度1024是预估的接收到的数据长度,要求该长度必须大于或等于接收到的数据长度,然后以该缓冲数组为基础,构造数据包对象,使用连接对象ds的receive方法接收反馈数据,由于在Java语言中,除String以外的其它对象都是按照地址传递,所以在receive方法内部可以改变数据包对象receiveDp的内容,这里的receiveDp的功能和返回值类似。数据接收到以后,只需要从数据包对象中读取出来就可以了,使用DatagramPacket对象中的getData方法可以获得数据包对象的缓冲区数组,但是缓冲区数组的长度一般大于有效数据的长度,换句话说,也就是缓冲区数组中只有一部分数据是反馈数据,所以需要使用DatagramPacket对象中的getLength方法获得有效数据的长度,则有效数据就是缓冲数组中的前有效数据长度个内容,这些才是真正的服务器端反馈的数据的内容。

    UDP方式客户端网络编程的最后一个步骤就是关闭连接。虽然UDP方式不建立专用的虚拟连接,但是连接对象还是需要占用系统资源,所以在使用完成以后必须关闭连接。关闭连接使用连接对象中的close方法即可,实现的代码如下:

                       ds.close();
    

    需要说明的是,和TCP建立连接的方式不同,UDP方式的同一个网络连接对象,可以发送到达不同服务器端IP或端口的数据包,这点是TCP方式无法做到的。

    介绍完了UDP方式客户端网络编程的基础知识以后,下面再来介绍一下UDP方式服务器端网络编程的基础知识。

    UDP方式网络编程的服务器端实现和TCP方式的服务器端实现类似,也是服务器端监听某个端口,然后获得数据包,进行逻辑处理以后将处理以后的结果反馈给客户端,最后关闭网络连接,下面依次进行介绍。

    首先UDP方式服务器端网络编程需要建立一个连接,该连接监听某个端口,实现的代码为:

        DatagramSocket ds = new DatagramSocket(10010);
    

    由于服务器端的端口需要固定,所以一般在建立服务器端连接时,都指定端口号。例如该示例代码中指定10010端口为服务器端使用的端口号,客户端端在连接服务器端时连接该端口号即可。

    接着服务器端就开始接收客户端发送过来的数据,其接收的方法和客户端接收的方法一直,其中receive方法的作用类似于TCP方式中accept方法的作用,该方法也是一个阻塞方法,其作用是接收数据。

    接收到客户端发送过来的数据以后,服务器端对该数据进行逻辑处理,然后将处理以后的结果再发送给客户端,在这里发送时就比客户端要麻烦一些,因为服务器端需要获得客户端的IP和客户端使用的端口号,这个都可以从接收到的数据包中获得。示例代码如下:

        //获得客户端的IP
    
         InetAddress clientIP = receiveDp.getAddress();
    
             //获得客户端的端口号
    
             Int clientPort = receiveDp.getPort();
    

    使用以上代码,就可以从接收到的数据包对象receiveDp中获得客户端的IP地址和客户端的端口号,这样就可以在服务器端中将处理以后的数据构造成数据包对象,然后将处理以后的数据内容反馈给客户端了。

    最后,当服务器端实现完成以后,关闭服务器端连接,实现的方式为调用连接对象的close方法,示例代码如下:

          ds.close();
    

    介绍完了UDP方式下的客户端编程和服务器端编程的基础知识以后,下面通过一个简单的示例演示UDP网络编程的基本使用。

    该示例的功能是实现将客户端程序的系统时间发送给服务器端,服务器端接收到时间以后,向客户端反馈字符串“OK”。实现该功能的客户端代码如下所示:

    package udp;
    
    import java.net.*;
    
    import java.util.*;
    
    /**
    
     * 简单的UDP客户端,实现向服务器端发生系统时间功能
    
     */
    
    public class SimpleUDPClient {
    
                public static void main(String[] args) {
    
                         DatagramSocket ds = null; //连接对象
    
                         DatagramPacket sendDp; //发送数据包对象
    
                    DatagramPacket receiveDp; //接收数据包对象
    
                         String serverHost = "127.0.0.1"; //服务器IP
    
                    int serverPort = 10010; //服务器端口号
    
                         try{
    
                            //建立连接
    
                            ds = new DatagramSocket();
    
                            //初始化发送数据
    
                            Date d = new Date(); //当前时间
    
                            String content = d.toString(); //转换为字符串
    
                            byte[] data = content.getBytes();
    
                            //初始化发送包对象
    
                            InetAddress address = InetAddress.getByName(serverHost);
    
                            sendDp = new DatagramPacket(data,data.length,address,serverPort);
    
                            //发送
    
                            ds.send(sendDp);
    
                                                                              
    
                            //初始化接收数据
    
                            byte[] b = new byte[1024];
    
                            receiveDp = new DatagramPacket(b,b.length);
    
                            //接收
    
                            ds.receive(receiveDp);
    
                            //读取反馈内容,并输出
    
                            byte[] response = receiveDp.getData();
    
                            int len = receiveDp.getLength();
    
                            String s = new String(response,0,len);
    
                            System.out.println("服务器端反馈为:" + s);
    
                    }catch(Exception e){
    
                            e.printStackTrace();
    
                    }finally{
    
                            try{
    
                               //关闭连接
    
                               ds.close();
    
                            }catch(Exception e){}
    
                    }
    
                }
    
            }
    

    在该示例代码中,首先建立UDP方式的网络连接,然后获得当前系统时间,这里获得的系统时间是客户端程序运行的本地计算机的时间,然后将时间字符串以及服务器端的IP和端口,构造成发送数据包对象,调用连接对象ds的send方法发送出去。在数据发送出去以后,构造接收数据的数据包对象,调用连接对象ds的receive方法接收服务器端的反馈,并输出在控制台。最后在finally语句块中关闭客户端网络连接。

    和下面将要介绍的服务器端一起运行时,客户端程序的输出结果为:

      服务器端反馈为:OK
    

    下面是该示例程序的服务器端代码实现:

          package udp;
    
            import java.net.*;
    
            /**
    
             * 简单UDP服务器端,实现功能是输出客户端发送数据,
    
               并反馈字符串“OK"给客户端
    
             */
    
            public class SimpleUDPServer {
    
                public static void main(String[] args) {
    
                         DatagramSocket ds = null; //连接对象
    
                         DatagramPacket sendDp; //发送数据包对象
    
                         DatagramPacket receiveDp; //接收数据包对象
    
                         final int PORT = 10010; //端口
    
                                                   try{
    
                            //建立连接,监听端口
    
                            ds = new DatagramSocket(PORT);
    
                           System.out.println("服务器端已启动:");
    
                            //初始化接收数据
    
                            byte[] b = new byte[1024];
    
                            receiveDp = new DatagramPacket(b,b.length);
    
                            //接收
    
                            ds.receive(receiveDp);
    
                            //读取反馈内容,并输出
    
                            InetAddress clientIP = receiveDp.getAddress();
    
                            int clientPort = receiveDp.getPort();
    
                            byte[] data = receiveDp.getData();
    
                            int len = receiveDp.getLength();
    
                            System.out.println("客户端IP:" + clientIP.getHostAddress());
    
                            System.out.println("客户端端口:" + clientPort);
    
                            System.out.println("客户端发送内容:" + new String(data,0,len));
    
                                                                              
    
                            //发送反馈
    
                            String response = "OK";
    
                            byte[] bData = response.getBytes();
    
                            sendDp = new DatagramPacket(bData,bData.length,clientIP,clientPort);
    
                            //发送
    
                            ds.send(sendDp);
    
                                                   }catch(Exception e){
    
                            e.printStackTrace();
    
                                                   }finally{
    
                            try{
    
                               //关闭连接
    
                               ds.close();
    
                            }catch(Exception e){}
    
                                                   }
    
                }
    
            }
    

    在该服务器端实现中,首先监听10010号端口,和TCP方式的网络编程类似,服务器端的receive方法是阻塞方法,如果客户端不发送数据,则程序会在该方法处阻塞。当客户端发送数据到达服务器端时,则接收客户端发送过来的数据,然后将客户端发送的数据内容读取出来,并在服务器端程序中打印客户端的相关信息,从客户端发送过来的数据包中可以读取出客户端的IP以及客户端端口号,将反馈数据字符串“OK”发送给客户端,最后关闭服务器端连接,释放占用的系统资源,完成程序功能示例。

    和前面TCP方式中的网络编程类似,这个示例也仅仅是网络编程的功能示例,也存在前面介绍的客户端无法进行多次数据交换,以及服务器端不支持多个客户端的问题,这两个问题也需要对于代码进行处理才可以很方便的进行解决。

    在解决该问题以前,需要特别指出的是UDP方式的网络编程由于不建立虚拟的连接,所以在实际使用时和TCP方式存在很多的不同,最大的一个不同就是“无状态”。该特点指每次服务器端都收到信息,但是这些信息和连接无关,换句话说,也就是服务器端只是从信息是无法识别出是谁发送的,这样就要求发送信息时的内容需要多一些,这个在后续的示例中可以看到。

    下面是实现客户端多次发送以及服务器端支持多个数据包同时处理的程序结构,实现的原理和TCP方式类似,在客户端将数据的发送和接收放入循环中,而服务器端则将接收到的每个数据包启动一个专门的线程进行处理。实现的代码如下:

     package udp;
    
        import java.net.*;
    
        import java.util.*;
    
        /**
    
         * 简单的UDP客户端,实现向服务器端发生系统时间功能
    
         * 该程序发送3次数据到服务器端
    
         */
    
        public class MulUDPClient {
    
                      public static void main(String[] args) {
    
                     DatagramSocket ds = null; //连接对象
    
                                         DatagramPacket sendDp; //发送数据包对象
    
                                         DatagramPacket receiveDp; //接收数据包对象
    
                                         String serverHost = "127.0.0.1"; //服务器IP
    
                                         int serverPort = 10012; //服务器端口号
    
                                         try{
    
                        //建立连接
    
                        ds = new DatagramSocket();
    
                        //初始化
    
                                  InetAddress address = InetAddress.getByName(serverHost);
    
                        byte[] b = new byte[1024];
    
                        receiveDp = new DatagramPacket(b,b.length);
    
                        System.out.println("客户端准备完成");
    
                        //循环30次,每次间隔0.01秒
    
                        for(int i = 0;i < 30;i++){
    
                                                            //初始化发送数据
    
                                                            Date d = new Date(); //当前时间
    
                                                            String content = d.toString(); //转换为字符串
    
                                                            byte[] data = content.getBytes();
    
                                                            //初始化发送包对象
    
                                                            sendDp = new DatagramPacket(data,data.length,address, serverPort);
    
                                                            //发送
    
                                                            ds.send(sendDp);
    
                                                            //延迟
    
                                                            Thread.sleep(10);
    
                                                            //接收
    
                                                            ds.receive(receiveDp);
    
                                                            //读取反馈内容,并输出
    
                                                            byte[] response = receiveDp.getData();
    
                                                            int len = receiveDp.getLength();
    
                                                            String s = new String(response,0,len);
    
                                                            System.out.println("服务器端反馈为:" + s);
    
                         }
    
                     }catch(Exception e){
    
                         e.printStackTrace();
    
                     }finally{
    
                         try{
    
                                                            //关闭连接
    
                                                            ds.close();
    
                         }catch(Exception e){}
    
                     }
    
             }
    
         }
    

    在该示例中,将和服务器端进行数据交换的逻辑写在一个for循环的内部,这样就可以实现和服务器端的多次交换了,考虑到服务器端的响应速度,在每次发送之间加入0.01秒的时间间隔。最后当数据交换完成以后关闭连接,结束程序。

    实现该逻辑的服务器端程序代码如下:

    package udp;
    
    import java.net.*;
    
    /**
    
    * 可以并发处理数据包的服务器端
    
    * 功能为:显示客户端发送的内容,并向客户端反馈字符串“OK”
    
    */
    
    public class MulUDPServer {
    
    public static void main(String[] args) {
    
    DatagramSocket ds = null; //连接对象
    
    DatagramPacket receiveDp; //接收数据包对象
    
    final int PORT = 10012; //端口
    
    byte[] b = new byte[1024];
    
    receiveDp = new DatagramPacket(b,b.length);
    
    try{
    
    //建立连接,监听端口
    
    ds = new DatagramSocket(PORT);
    
    System.out.println("服务器端已启动:");
    
    while(true){
    
    //接收
    
    ds.receive(receiveDp);
    
    //启动线程处理数据包
    
    new LogicThread(ds,receiveDp);
    
    }
    
    }catch(Exception e){
    
             e.printStackTrace();
    
    }finally{
    
    try{
    
    //关闭连接
    
    ds.close();
    
    }catch(Exception e){}
    
    }
    
    }
    
    }
    

    该代码实现了服务器端的接收逻辑,使用一个循环来接收客户端发送过来的数据包,当接收到数据包以后启动一个LogicThread线程处理该数据包。这样服务器端就可以实现同时处理多个数据包了。

    实现逻辑处理的线程代码如下:

    package udp;
    
    import java.net.*;
    
    /**
    
     * 逻辑处理线程
    
     */
    
    public class LogicThread extends Thread {
    
    /**连接对象*/
    
    DatagramSocket ds;
    
    /**接收到的数据包*/
    
    DatagramPacket dp;
    
     
    
    public LogicThread(DatagramSocket ds,DatagramPacket dp){
    
    this.ds = ds;
    
    this.dp = dp;
    
    start(); //启动线程
    
    }
    
     
    
    public void run(){
    
    try{
    
    //获得缓冲数组
    
    byte[] data = dp.getData();
    
    //获得有效数据长度
    
    int len = dp.getLength();
    
    //客户端IP
    
    InetAddress clientAddress = dp.getAddress();
    
    //客户端端口
    
    int clientPort = dp.getPort();
    
    //输出
    
    System.out.println("客户端IP:" + clientAddress.getHostAddress());
    
    System.out.println("客户端端口号:" + clientPort);
    
    System.out.println("客户端发送内容:" + new String(data,0,len));
    
    //反馈到客户端
    
    byte[] b = "OK".getBytes();
    
    DatagramPacket sendDp = new DatagramPacket(b,b.length,clientAddress,clientPort);
    
    //发送
    
    ds.send(sendDp);
    
    }catch(Exception e){
    
    e.printStackTrace();
    
    }
    
    }
    
    }
    

    在该线程中,只处理一次UDP通讯,当通讯结束以后线程死亡,在线程内部,每次获得客户端发送过来的信息,将获得的信息输出到服务器端程序的控制台,然后向客户端反馈字符串“OK”。

    由于UDP数据传输过程中可能存在丢失,所以在运行该程序时可能会出现程序阻塞的情况。如果需要避免该问题,可以将客户端的网络发送部分也修改成线程实现。

    关于基础的UDP网络编程就介绍这么多了,下面将介绍一下网络协议的概念。

    ###网络协议

    对于需要从事网络编程的程序员来说,网络协议是一个需要深刻理解的概念。那么什么是网络协议呢?

    网络协议是指对于网络中传输的数据格式的规定。对于网络编程初学者来说,没有必要深入了解TCP/IP协议簇,所以对于初学者来说去读大部头的《TCP/IP协议》也不是一件很合适的事情,因为深入了解TCP/IP协议是网络编程提高阶段,也是深入网络编程底层时才需要做的事情。

    对于一般的网络编程来说,更多的是关心网络上传输的逻辑数据内容,也就是更多的是应用层上的网络协议,所以后续的内容均以实际应用的数据为基础来介绍网络协议的概念。

    那么什么是网络协议呢,下面看一个简单的例子。春节晚会上“小沈阳”和赵本山合作的小品《不差钱》中,小沈阳和赵本山之间就设计了一个协议,协议的内容为:

           如果点的菜价钱比较贵是,就说没有。
    

    按照该协议的规定,就有了下面的对话:

           赵本山:4斤的龙虾
    
           小沈阳:(经过判断,得出价格比较高),没有
    
           赵本山:鲍鱼
    
           小沈阳:(经过判断,得出价格比较高),没有
    

    这就是一种双方达成的一种协议约定,其实这种约定的实质和网络协议的实质是一样的。网络协议的实质也是客户端程序和服务器端程序对于数据的一种约定,只是由于以计算机为基础,所以更多的是使用数字来代表内容,这样就显得比较抽象一些。

    下 面再举一个简单的例子,介绍一些基础的网络协议设计的知识。例如需要设计一个简单的网络程序:网络计算器。也就是在客户端输入需要计算的数字和运算符,在 服务器端实现计算,并将计算的结果反馈给客户端。在这个例子中,就需要约定两个数据格式:客户端发送给服务器端的数据格式,以及服务器端反馈给客户端的数 据格式。

    可能你觉得这个比较简单,例如客户端输入的数字依次是12和432,输入的运算符是加号,可能最容易想到的数据格式是形成字符串“12+432”,这样格式的确比较容易阅读,但是服务器端在进行计算时,逻辑就比较麻烦,因为需要首先拆分该字符串,然后才能进行计算,所以可用的数据格式就有了一下几种:

              “12,432,+”     格式为:第一个数字,第二个数字,运算符
    
              “12,+,432”     格式为:第一个数字,运算符,第二个数字
    

    其实以上两种数据格式很接近,比较容易阅读,在服务器端收到该数据格式以后,使用“,”为分隔符分割字符串即可。

    假设对于运算符再进行一次约定,例如约定数字0代表+,1代表减,2代表乘,3代表除,整体格式遵循以上第一种格式,则上面的数字生产的协议数据为:

          “12,432,0”
    

    这就是一种基本的发送的协议约定了。

    另 外一个需要设计的协议格式就是服务器端反馈的数据格式,其实服务器端主要反馈计算结果,但是在实际接受数据时,有可能存在格式错误的情况,这样就需要简单 的设计一下服务器端反馈的数据格式了。例如规定,如果发送的数据格式正确,则反馈结果,否则反馈字符串“错误”。这样就有了以下的数据格式:

         客户端:“1,111,1”         服务器端:”-110”
    
         客户端:“123,23,0”    服务器端:“146”
    
         客户端:“1,2,5”       服务器端:“错误”
    

    这样就设计出了一种最最基本的网络协议格式,从该示例中可以看出,网络协议就是一种格式上的约定,可以根据逻辑的需要约定出各种数据格式,在进行设计时一般遵循“简单、通用、容易解析”的原则进行。

    而对于复杂的网络程序来说,需要传输的数据种类和数据量都比较大,这样只需要依次设计出每种情况下的数据格式即可,例如QQ程序,在该程序中需要进行传输的网络数据种类很多,那么在设计时就可以遵循:登录格式、注册格式、发送消息格式等等,一一进行设计即可。所以对于复杂的网络程序来说,只是增加了更多的命令格式,在实际设计时的工作量增加不是太大。

    不管怎么说,在网络编程中,对于同一个网络程序来说,一般都会涉及到两个网络协议格式:客户端发送数据格式和服务器端反馈数据格式,在实际设计时,需要一一对应。这就是最基本的网络协议的知识。

    网络协议设计完成以后,在进行网络编程时,就需要根据设计好的协议格式,在程序中进行对应的编码了,客户端程序和服务器端程序需要进行协议处理的代码分别如下。

    客户端程序需要完成的处理为:

    • 客户端发送协议格式的生成

    • 服务器端反馈数据格式的解析

    服务器端程序需要完成的处理为:

    • 服务器端反馈协议格式的生成

    • 客户端发送协议格式的解析

    这里的生成是指将计算好的数据,转换成规定的数据格式,这里的解析指,从反馈的数据格式中拆分出需要的数据。在进行对应的代码编写时,严格遵循协议约定即可。

    所以,对于程序员来说,在进行网络程序编写时,需要首先根据逻辑的需要设计网络协议格式,然后遵循协议格式约定进行协议生成和解析代码的编写,最后使用网络编程技术实现整个网络编程的功能。

    由于各种网络程序使用不同的协议格式,所以不同网络程序的客户端之间无法通用。

    而对于常见协议的格式,例如HTTP(Hyper Text Transfer Protocol,超文本传输协议)、FTP(File Transfer Protocol,文件传输协议),SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)等等,都有通用的规定,具体可以查阅相关的RFC文档。

    最后,对于一种网络程序来说,网络协议格式是该程序最核心的技术秘密,因为一旦协议格式泄漏,则任何一个人都可以根据该格式进行客户端的编写,这样将影响服务器端的实现,也容易出现一些其它的影响。

    ####1.2.6小结

    关于网络编程基本的技术就介绍这么多,该部分介绍了网络编程的基础知识,以及Java语言对于网络编程的支持,网络编程的步骤等,并详细介绍了TCP方式网络编程和UDP方式网络编程在Java语言中的实现。

    网络协议也是网络程序的核心,所以在实际开始进行网络编程时,设计一个良好的协议格式也是必须进行的工作。
    ###网络编程示例

    “实践出真知”,所以在进行技术学习时,还是需要进行很多的练习,才可以体会技术的奥妙,下面通过两个简单的示例,演示网络编程的实际使用。

    ####1.3.1质数判别示例

    该示例实现的功能是质数判断,程序实现的功能为客户端程序接收用户输入的数字,然后将用户输入的内容发送给服务器端,服务器端判断客户端发送的数字是否是质数,并将判断的结果反馈给客户端,客户端根据服务器端的反馈显示判断结果。

    质数的规则是:最小的质数是2,只能被1和自身整除的自然数。当用户输入小于2的数字,以及输入的内容不是自然数时,都属于非法输入。

    网络程序的功能都分为客户端程序和服务器端程序实现,下面先描述一下每个程序分别实现的功能:

    1、 客户端程序功能:

    • 接收用户控制台输入

    • 判断输入内容是否合法

    • 按照协议格式生成发送数据

    • 发送数据

    • 接收服务器端反馈

    • 解析服务器端反馈信息,并输出

    2、 服务器端程序功能:

    • 接收客户端发送数据

    • 按照协议格式解析数据

    • 判断数字是否是质数

    • 根据判断结果,生成协议数据

    • 将数据反馈给客户端

    分解好了网络程序的功能以后,就可以设计网络协议格式了,如果该程序的功能比较简单,所以设计出的协议格式也不复杂。

    客户端发送协议格式:

    • 将用户输入的数字转换为字符串,再将字符串转换为byte数组即可。

    • 例如用户输入16,则转换为字符串“16”,使用getBytes转换为byte数组。

    • 客户端发送“quit”字符串代表结束连接

    服务器端发送协议格式:

    • 反馈数据长度为1个字节。数字0代表是质数,1代表不是质数,2代表协议格式错误。

    • 例如客户端发送数字12,则反馈1,发送13则反馈0,发送0则反馈2。

    功能设计完成以后,就可以分别进行客户端和服务器端程序的编写了,在编写完成以后联合起来进行调试即可。

    下面分别以TCP方式和UDP方式实现该程序,注意其实现上的差异。不管使用哪种方式实现,客户端都可以多次输入数据进行判断。对于UDP方式来说,不需要向服务器端发送quit字符串。

    以TCP方式实现的客户端程序代码如下:

    package example1;
    
    import java.io.*;
    
    import java.net.*;
    
    /**
    
     * 以TCP方式实现的质数判断客户端程序
    
     */
    
    public class TCPPrimeClient {
    
             static BufferedReader br;
    
             static Socket socket;
    
             static InputStream is;
    
             static OutputStream os;
    
             /**服务器IP*/
    
             final static String HOST = "127.0.0.1";
    
             /**服务器端端口*/
    
             final static int PORT = 10005;
    
            
    
             public static void main(String[] args) {
    
                       init(); //初始化
    
                       while(true){
    
                                System.out.println("请输入数字:");
    
                                String input = readInput(); //读取输入
    
                                if(isQuit(input)){ //判读是否结束
    
                                         byte[] b = "quit".getBytes();
    
                                         send(b);
    
                                         break; //结束程序
    
                                }
    
                                if(checkInput(input)){ //校验合法
    
                                         //发送数据
    
                                         send(input.getBytes());
    
                                         //接收数据
    
                                         byte[] data = receive();
    
                                         //解析反馈数据
    
                                         parse(data);
    
                                }else{
    
                                         System.out.println("输入不合法,请重新输入!");
    
                                }
    
                       }
    
                       close(); //关闭流和连接
    
             }
    
            
    
             /**
    
              * 初始化
    
              */
    
             private static void init(){
    
                       try {
    
                                br = new BufferedReader(
    
                                                   new InputStreamReader(System.in));
    
                                socket = new Socket(HOST,PORT);
    
                                is = socket.getInputStream();
    
                                os = socket.getOutputStream();
    
                       } catch (Exception e) {}
    
             }
    
            
    
             /**
    
              * 读取客户端输入
    
              */
    
             private static String readInput(){
    
                       try {
    
                                return br.readLine();
    
                       } catch (Exception e) {
    
                                return null;
    
                       }
    
             }
    
            
    
             /**
    
              * 判断是否输入quit
    
              * @param input 输入内容
    
              * @return true代表结束,false代表不结束
    
              */
    
             private static boolean isQuit(String input){
    
                       if(input == null){
    
                                return false;
    
                       }else{
    
                                if("quit".equalsIgnoreCase(input)){
    
                                         return true;
    
                                }else{
    
                                         return false;
    
                                }
    
                       }
    
             }
    
            
    
             /**
    
              * 校验输入
    
              * @param input 用户输入内容
    
              * @return true代表输入符合要求,false代表不符合
    
              */
    
             private static boolean checkInput(String input){
    
                       if(input == null){
    
                                return false;
    
                       }
    
                       try{
    
                                int n = Integer.parseInt(input);
    
                                if(n >= 2){
    
                                         return true;
    
                                }else{
    
                                         return false;
    
                                }
    
                       }catch(Exception e){
    
                                return false; //输入不是整数
    
                       }
    
             }
    
            
    
             /**
    
              * 向服务器端发送数据
    
              * @param data 数据内容
    
              */
    
             private static void send(byte[] data){
    
                       try{
    
                                os.write(data);
    
                       }catch(Exception e){}
    
             }
    
            
    
             /**
    
              * 接收服务器端反馈
    
              * @return 反馈数据
    
              */
    
             private static byte[] receive(){
    
                       byte[] b = new byte[1024];
    
                       try {
    
                                int n = is.read(b);
    
                                byte[] data = new byte[n];
    
                                //复制有效数据
    
                                System.arraycopy(b, 0, data, 0, n);
    
                                return data;
    
                       } catch (Exception e){}
    
                       return null;
    
             }
    
            
    
             /**
    
              * 解析协议数据
    
              * @param data 协议数据
    
              */
    
             private static void parse(byte[] data){
    
                       if(data == null){
    
                                System.out.println("服务器端反馈数据不正确!");
    
                                return;
    
                       }
    
                       byte value = data[0]; //取第一个byte
    
                       //按照协议格式解析
    
                       switch(value){
    
                       case 0:
    
                                System.out.println("质数");
    
                                break;
    
                       case 1:
    
                                System.out.println("不是质数");
    
                                break;
    
                       case 2:
    
                                System.out.println("协议格式错误");
    
                                break;
    
                       }
    
             }
    
            
    
             /**
    
              * 关闭流和连接
    
              */
    
             private static void close(){
    
                       try{
    
                                br.close();
    
                                is.close();
    
                                os.close();
    
                                socket.close();
    
                       }catch(Exception e){
    
                                e.printStackTrace();
    
                       }
    
             }
    
    }
    

    在该代码中,将程序的功能使用方法进行组织,使得结构比较清晰,核心的逻辑流程在main方法中实现。

    以TCP方式实现的服务器端的代码如下:

    package example1;
    
    import java.net.*;
    
    /**
    
     * 以TCP方式实现的质数判别服务器端
    
     */
    
    public class TCPPrimeServer {
    
             public static void main(String[] args) {
    
                       final int PORT = 10005;
    
                       ServerSocket ss = null;
    
                       try {
    
                                ss = new ServerSocket(PORT);
    
                                System.out.println("服务器端已启动:");
    
                                while(true){
    
                                         Socket s = ss.accept();
    
                                         new PrimeLogicThread(s);
    
                                }
    
                       } catch (Exception e) {}
    
                       finally{
    
                                try {
    
                                         ss.close();
    
                                } catch (Exception e2) {}
    
                       }
    
                      
    
             }
    
    }
    
    package example1;
    
    import java.io.*;
    
    import java.net.*;
    
    /**
    
     * 实现质数判别逻辑的线程
    
     */
    
    public class PrimeLogicThread extends Thread {
    
             Socket socket;
    
             InputStream is;
    
             OutputStream os;
    
            
    
             public PrimeLogicThread(Socket socket){
    
                       this.socket = socket;
    
                       init();
    
                       start();
    
             }
    
             /**
    
              * 初始化
    
              */
    
             private void init(){
    
                       try{
    
                                is = socket.getInputStream();
    
                                os = socket.getOutputStream();
    
                       }catch(Exception e){}
    
             }
    
            
    
             public void run(){
    
                       while(true){
    
                                //接收客户端反馈
    
                                byte[] data = receive();
    
                                //判断是否是退出
    
                                if(isQuit(data)){
    
                                         break; //结束循环
    
                                }
    
                                //逻辑处理
    
                                byte[] b = logic(data);
    
                                //反馈数据
    
                                send(b);
    
                       }
    
                       close();
    
             }
    
            
    
             /**
    
              * 接收客户端数据
    
              * @return 客户端发送的数据
    
              */
    
             private byte[] receive(){
    
                       byte[] b = new byte[1024];
    
                       try {
    
                                int n = is.read(b);
    
                                byte[] data = new byte[n];
    
                                //复制有效数据
    
                                System.arraycopy(b, 0, data, 0, n);
    
                                return data;
    
                       } catch (Exception e){}
    
                       return null;
    
             }
    
            
    
             /**
    
              * 向客户端发送数据
    
              * @param data 数据内容
    
              */
    
             private void send(byte[] data){
    
                       try{
    
                                os.write(data);
    
                       }catch(Exception e){}
    
             }
    
            
    
             /**
    
              * 判断是否是quit
    
              * @return 是返回true,否则返回false
    
              */
    
             private boolean isQuit(byte[] data){
    
                       if(data == null){
    
                                return false;
    
                       }else{
    
                                String s = new String(data);
    
                                if(s.equalsIgnoreCase("quit")){
    
                                         return true;
    
                                }else{
    
                                         return false;
    
                                }
    
                       }
    
             }
    
            
    
             private byte[] logic(byte[] data){
    
                       //反馈数组
    
                       byte[] b = new byte[1];
    
                       //校验参数
    
                       if(data == null){
    
                                b[0] = 2;
    
                                return b;
    
                       }
    
                       try{
    
                                //转换为数字
    
                                String s = new String(data);
    
                                int n = Integer.parseInt(s);
    
                                //判断是否是质数
    
                                if(n >= 2){
    
                                         boolean flag = isPrime(n);
    
                                         if(flag){
    
                                                   b[0] = 0;
    
                                         }else{
    
                                                   b[0] = 1;
    
                                         }
    
                                }else{
    
                                         b[0] = 2; //格式错误
    
                                         System.out.println(n);
    
                                }
    
                       }catch(Exception e){
    
                                e.printStackTrace();
    
                                b[0] = 2;
    
                       }
    
                       return b;
    
             }
    
            
    
             /**
    
              *
    
              * @param n
    
              * @return
    
              */
    
             private boolean isPrime(int n){
    
                       boolean b = true;
    
                       for(int i = 2;i <= Math.sqrt(n);i++){
    
                                if(n % i == 0){
    
                                         b = false;
    
                                         break;
    
                                }
    
                       }
    
                       return b;
    
             }
    
            
    
             /**
    
              * 关闭连接
    
              */
    
             private void close(){
    
                       try {
    
                                is.close();
    
                                os.close();
    
                                socket.close();
    
                       } catch (Exception e){}
    
             }
    
    }
    

    本示例使用的服务器端的结构和前面示例中的结构一致,只是逻辑线程的实现相对来说要复杂一些,在线程类中的logic方法中实现了服务器端逻辑,根据客户端发送过来的数据,判断是否是质数,然后根据判断结果按照协议格式要求,生成客户端反馈数据,实现服务器端要求的功能。

    ###猜数字小游戏

    下面这个示例是一个猜数字的控制台小游戏。该游戏的规则是:当客户端第一次连接到服务器端时,服务器端生产一个【0,50】之间的随机数字,然后客户端输入数字来猜该数字,每次客户端输入数字以后,发送给服务器端,服务器端判断该客户端发送的数字和随机数字的关系,并反馈比较结果,客户端总共有5次猜的机会,猜中时提示猜中,当输入”quit”时结束程序。

    和前面的示例类似,在进行网络程序开发时,首先需要分解一下功能的实现,觉得功能是在客户端程序中实现还是在服务器端程序中实现。区分的规则一般是:客户端 程序实现接收用户输入等界面功能,并实现一些基础的校验降低服务器端的压力,而将程序核心的逻辑以及数据存储等功能放在服务器端进行实现。遵循该原则划分 的客户端和服务器端功能如下所示。

    客户端程序功能列表:

    • 接收用户控制台输入

    • 判断输入内容是否合法

    • 按照协议格式发送数据

    • 根据服务器端的反馈给出相应提示

      服务器端程序功能列表:

    • 接收客户端发送数据

    • 按照协议格式解析数据

    • 判断发送过来的数字和随机数字的关系

    • 根据判断结果生产协议数据

    • 将生产的数据反馈给客户端

    在该示例中,实际使用的网络命令也只有两条,所以显得协议的格式比较简单。

    其中客户端程序协议格式如下:

    • 将用户输入的数字转换为字符串,然后转换为byte数组

    • 发送“quit”字符串代表退出

    其中服务器端程序协议格式如下:

    • 反馈长度为1个字节,数字0代表相等(猜中),1代表大了,2代表小了,其它数字代表错误。

    实现该程序的代码比较多,下面分为客户端程序实现和服务器端程序实现分别进行列举。

    客户端程序实现代码如下:

    package guess;
    
    import java.net.*;
    
    import java.io.*;
    
    /**
    
     * 猜数字客户端
    
     */
    
    public class TCPClient {
    
     public static void main(String[] args) {
    
             Socket socket = null;
    
             OutputStream os = null;
    
             InputStream is = null;
    
             BufferedReader br = null;
    
             byte[] data = new byte[2];
    
             try{
    
                       //建立连接
    
                       socket = new Socket(
    
                                         "127.0.0.1",10001);
    
                      
    
                       //发送数据
    
                       os= socket.getOutputStream();
    
                      
    
                       //读取反馈数据
    
                       is = socket.getInputStream();
    
                      
    
                       //键盘输入流
    
                       br = new BufferedReader(
    
                                         new InputStreamReader(System.in));
    
                      
    
                       //多次输入
    
                       while(true){
    
                                System.out.println("请输入数字:");
    
                                //接收输入
    
                                String s = br.readLine();
    
                                //结束条件
    
                                if(s.equals("quit")){
    
                                         os.write("quit".getBytes());
    
                                         break;
    
                                }
    
                                //校验输入是否合法
    
                                boolean b = true;
    
                                try{
    
                                         Integer.parseInt(s);
    
                                }catch(Exception e){
    
                                         b = false;
    
                                }
    
                                if(b){ //输入合法
    
                                         //发送数据
    
                                         os.write(s.getBytes());
    
                                         //接收反馈
    
                                         is.read(data);
    
                                         //判断
    
                                         switch(data[0]){
    
                                         case 0:
    
                                                   System.out.println("相等!祝贺你!");
    
                                                   break;
    
                                         case 1:
    
                                                   System.out.println("大了!");
    
                                                   break;
    
                                         case 2:
    
                                                   System.out.println("小了!");
    
                                                   break;
    
                                         default:
    
                                                   System.out.println("其它错误!");
    
                                         }
    
                                         //提示猜的次数
    
                                         System.out.println("你已经猜了" + data[1] + "次!");
    
                                         //判断次数是否达到5次
    
                                         if(data[1] >= 5){
    
                                                   System.out.println("你挂了!");
    
                                                   //给服务器端线程关闭的机会
    
                                                   os.write("quit".getBytes());
    
                                                   //结束客户端程序
    
                                                   break;
    
                                         }
    
                                }else{ //输入错误
    
                                         System.out.println("输入错误!");
    
                                }
    
                       }
    
             }catch(Exception e){
    
                       e.printStackTrace();
    
             }finally{
    
                       try{
    
                                //关闭连接
    
                                br.close();
    
                                is.close();
    
                                os.close();
    
                                socket.close();
    
                       }catch(Exception e){
    
                                e.printStackTrace();
    
                       }
    
             }
    
     }
    
          }
    

    在该示例中,首先建立一个到IP地址为127.0.0.1的端口为10001的连接,然后进行各个流的初始化工作,将逻辑控制的代码放入在一个while循环中,这样可以在客户端多次进行输入。在循环内部,首先判断用户输入的是否为quit字符串,如果是则结束程序,如果输入不是quit,则首先校验输入的是否是数字,如果不是数字则直接输出“输入错误!”并继续接收用户输入,如果是数字则发送给服务器端,并根据服务器端的反馈显示相应的提示信息。最后关闭流和连接,结束客户端程序。

    服务器端程序的实现还是分为服务器控制程序和逻辑线程,实现的代码分别如下:

    package guess;
    
    import java.net.*;
    
    /**
    
     * TCP连接方式的服务器端
    
     * 实现功能:接收客户端的数据,判断数字关系
    
     */
    
    public class TCPServer {
    
     public static void main(String[] args) {
    
             try{
    
                       //监听端口
    
                       ServerSocket ss = new ServerSocket(10001);
    
                       System.out.println("服务器已启动:");
    
                       //逻辑处理
    
                       while(true){
    
                                //获得连接
    
                                Socket s = ss.accept();
    
                                //启动线程处理
    
                                new LogicThread(s);
    
                       }
    
                      
    
             }catch(Exception e){
    
                       e.printStackTrace();
    
             }
    
     }
    
         }
    
          package guess;
    
    import java.net.*;
    
    import java.io.*;
    
    import java.util.*;
    
    /**
    
     * 逻辑处理线程
    
     */
    
    public class LogicThread extends Thread {
    
          Socket s;
    
         
    
          static Random r = new Random();
    
         
    
          public LogicThread(Socket s){
    
                  this.s = s;
    
                  start(); //启动线程
    
          }
    
         
    
          public void run(){
    
                  //生成一个[0,50]的随机数
    
                  int randomNumber = Math.abs(r.nextInt() % 51);
    
                  //用户猜的次数
    
                  int guessNumber = 0;
    
                  InputStream is = null;
    
                  OutputStream os = null;
    
                  byte[] data = new byte[2];
    
                  try{
    
                           //获得输入流
    
                           is = s.getInputStream();
    
                           //获得输出流
    
                           os = s.getOutputStream();
    
                           while(true){ //多次处理
    
                                     //读取客户端发送的数据
    
                                     byte[] b = new byte[1024];
    
                                     int n = is.read(b);
    
                                     String send = new String(b,0,n);
    
                                     //结束判别
    
                                     if(send.equals("quit")){
    
                                              break;
    
                                     }
    
                                     //解析、判断
    
                                     try{
    
                                              int num = Integer.parseInt(send);
    
                                              //处理
    
                                              guessNumber++; //猜的次数增加1
    
                                              data[1] = (byte)guessNumber;
    
                                              //判断
    
                                              if(num > randomNumber){
    
                                                       data[0] = 1;
    
                                              }else if(num < randomNumber){
    
                                                       data[0] = 2;
    
                                              }else{
    
                                                       data[0] = 0;
    
                                                       //如果猜对
    
                                                       guessNumber = 0; //清零
    
                                                       randomNumber = Math.abs(r.nextInt() % 51);
    
                                              }
    
                                              //反馈给客户端
    
                                              os.write(data);                                    
    
                                             
    
                                     }catch(Exception e){ //数据格式错误
    
                                              data[0] = 3;
    
                                              data[1] = (byte)guessNumber;
    
                                              os.write(data); //发送错误标识
    
                                              break;
    
                                     }
    
                                     os.flush();   //强制发送
    
                           }
    
                          
    
                  }catch(Exception e){
    
                           e.printStackTrace();
    
                  }finally{
    
                           try{
    
                                     is.close();
    
                                     os.close();
    
                                     s.close();
    
                           }catch(Exception e){}
    
                  }
    
          }
    
    }
    

    在 该示例中,服务器端控制部分和前面的示例中一样。也是等待客户端连接,如果有客户端连接到达时,则启动新的线程去处理客户端连接。在逻辑线程中实现程序的 核心逻辑,首先当线程执行时生产一个随机数字,然后根据客户端发送过来的数据,判断客户端发送数字和随机数字的关系,然后反馈相应的数字的值,并记忆客户 端已经猜过的次数,当客户端猜中以后清零猜过的次数,使得客户端程序可以继续进行游戏。

    总体来说,该程序示例的结构以及功能都与上一个程序比较类似,希望通过比较这两个程序,加深对于网络编程的认识,早日步入网络编程的大门。

    转自:http://www.cnblogs.com/springcsc/archive/2009/12/03/1616413.html
    文章有不当之处,欢迎指正,你也可以关注我的微信公众号:好好学java,获取优质资源。

    展开全文
  • 而实际在网络编程中很多情况都是在发送和接收数据时出现了socket上有异常导致操作无法完成,而返回值只能涉及到操作相关的字节数和是否错误,并不能反映完全的错误信息。 也不讨论windows上的错误信息获取,而主要...
  • 今天在学网络编程的时候,在基于TCP的网络编程中,想实现两台计算机之间的通信,一个是服务器端,一个是客户端,运行的时候出现了这样的错误错误信息 java.net.ConnectException: Connection refused: connect ...
  • 网络编程常见问题总结

    千次阅读 2011-02-21 16:23:00
     对于网络编程的更多详细说明建议参考下面的书籍 《UNIX网络编程》 《TCP/IP 详解》 《Unix环境高级编程》 非阻塞IO和阻塞IO: 在网络编程中对于一个网络句柄会遇到阻塞IO和非阻塞IO的概念, 这里对于这两种...
  • 手机本身是作为手机终端使用的,因此它的计算...Android完全支持JDK本身的TCP,UDP网络通信API,也可以使用ServerSocket,Socket来建立基于TCP/IP协议的网络通信,也可以使用DatagramSocket,Datagrampacket来建立基于UD
  • Java网络编程详解

    万次阅读 多人点赞 2014-09-10 17:55:34
    1、网络编程 网络编程  网络编程对于很多的初学者来说,都是很向往的一种编程技能,但是很多的初学者却因为很长一段时间无法进入网络编程的大门而放弃了对于该部分技术的学习。  在 学习网络编程以前...
  • C# 网络编程之套接字编程基础知识

    千次阅读 2013-07-13 22:08:28
    最近阅读了周存杰编写的《C#网络编程实例教程》并阅读了很多相关方面的资料,同时自己也做了一些套接字编程方面的C#程序,所以根据它的知识总结了最近的套接字编程的一些知识点,方便自己的理解与他人的学习,同时也...
  • 目录 一、OSI七层模型 二、TCP/IP模型 三、TCP/IP协议族 四、... 区分不同应用程序进程间的网络通信和连接,主要有3个参数:通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号。通过将这3个参数结合起来,...
  • 今天写一个简单的socket网络通讯的程序的时候,用ctrl+c结束服务器端程序之后,再次启动服务器出现了bind failed:the address already in use的错误。在网上查了一下以后找到了原因,在此记录一下。这个IBM的官网上...
  • Linux网络编程入门

    千次阅读 2017-06-30 03:01:00
    (一)Linux网络编程–网络知识介绍Linux网络编程–网络知识介绍 客户端和服务端 网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的–客户端和服务器端.客户端 在网络程序中,如果一个程序主动...
  • 网络编程 详解

    千次阅读 2017-05-08 11:19:05
    网络编程的学习主要是通过《UNIX网络编程》来学习的,也看了一下《Effective TCP/IP》和一些项目。网络编程在一个项目中占到的比重一般比较小,更重要的是基于网络的功能的实现,但几乎每一个项目都离不开网络。网络...
  • 网络编程学习小结

    万次阅读 热门讨论 2009-08-29 13:42:00
    几种网络编程方式:ISAPI、CGI、WinInet、Winsock它们之间的区别:1) ISAPI主要是开发基于浏览器客户端与服务器端程序。效率比CGI方式高,而且也扩展了CGI没有的一些功能。(基于TCP/IP模型中的应用层)2) CGI...
  • linux操作系统下c语言编程入门--网络编程Linux系统的一个主要特点是他的网络功能非常强大。随着网络的日益普及,基于网络的应用也将越来越多。 在这个网络时代,掌握了Linux的网络编程技术,将令每一个人处于...
  • Windows网络编程

    万次阅读 2007-12-13 16:58:00
    第一章 序言 我写这个专题的目的,一方面是为了通过对网络编程再一次系统的总结,提高自己的网络编程水平,特别是Windows下的网络编程水平。同时,我也希望,能为众多初学网络编程的人提供一点帮助,因为我开始学习...
  • 网络编程基础概述

    千次阅读 2011-12-27 19:59:16
    网络编程 网络编程对于很多的初学者来说,都是很向往的一种编程技能,但是很多的初学者却因为很长一段时间无法进入网络编程的大门而放弃了对于该部分技术的学习。 在 学习网络编程以前,很多初学者可能觉得网络...
  • 上一篇文章:【Linux】Linux网络编程(含常见服务器模型,上篇)。   高级嵌套字函数 前面介绍的一些函数(read、write等)都是网络程序里最基本的函数,也是最原始的通信函数。下面介绍一下几个网络编程的高级...
  • MFC 网络编程

    千次阅读 2014-07-18 19:36:04
    IP地址用于表示网络上的各个不同主机的节点,就像家庭住址一样,邮递员通过家庭住址以决定将该信件投往何处。IP地址是一个32位的二进制数。 IP地址包含两部分:网络号和主机号,又称为前缀地址和后缀地址。 IP协议...
  • Linux 网络编程—— libnet 使用指南

    万次阅读 2015-04-12 20:23:31
    概述 通过《原始套接字实例:发送 UDP 数据包》的学习,我们组 UDP 数据包时常考虑字节流顺序、校验和计算等问题,有时候会比较繁琐,那么,有没有一种更...libnet 的开发目的是:建立一个简单统一的网络编程接口以屏
  • Python Socket 网络编程

    万次阅读 2016-05-22 21:22:56
    Socket 是进程间通信的一种方式,它与其他进程...要解决网络上两台主机之间的进程通信问题,首先唯一标识该进程,在 TCP/IP 网络协议中,就是通过 (IP地址,协议,端口号) 三元组来标识进程的,解决了进程标识问题,
  • Java网络编程

    千次阅读 2018-10-24 17:54:18
    **Java网络编程** **网络技术基础** OSI模型 我们已经知道,计算机网络是处于不同地理位置的计算机系统通过通信设备和线路连接起来,以网络软件实现资源共享的系统。在计算机网络出现的早期,不同的软件、硬件...
  • 手把手叫你玩转网络编程系列之三  完成端口(Completion Port)详解  ----- By PiggyXP(小猪) 前 言  本系列里完成端口的代码在两年前就已经写好了,但是由于许久没有写东西了,不知该如何提笔,所以这篇...
  • QT TCP网络编程

    万次阅读 多人点赞 2017-03-22 20:36:45
    首先介绍一下TCP:(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。...一:客户端编程QT提供了QTcpSocket类,可以直接实例化一个客户端,可在h
  • 本文针对《网络编程 卷1》unp.h 头文件及其编译问题进行记录,在使用本书的源码时必须解决这个问题。本书的源码可在这里下载 http://www.unpbook.com。  首先进行编译,在目录 unpv13e 下依次执行以下步骤 ./...
  • 熟练掌握Linux下的TCP/IP网络编程,至少有三个层面的知识需要熟悉: 1. TCP/IP协议(如连接的建立和终止、重传和确认、滑动窗口和拥塞控制等等) 2. Socket I/O系统调用(重点如read/write),这是TCP/IP协议在...
  • 一、本文目的在涉及网络编程的实际项目应用中,由于网络不可能一直处于理想状态,TCP长连接也可能随时正常或异常地断开,如果不予处理,那么就可能因此而给程序带来很多潜在的问题。 编写该文档的目的就在于针对网络...
  • 最近在弄一个豆瓣API应用,在豆瓣的OAuth2.0认证过程中遇到了各种问题,同时自己需要一个个的尝试与解决,最终完成了豆瓣API的访问.作者这里就不再吐槽豆瓣的认证文档了,毕竟人家也不容易.但是作者发现关于豆瓣OAuth...
  • java网络编程+通讯协议

    千次阅读 2016-03-01 15:37:02
    网络编程对于很多的初学者来说,都是很向往的一种编程技能,但是很多的初学者却因为很长一段时间无法进入网络编程的大门而放弃了对于该部分技术的学习。   在 学习网络编程以前,很多初学者可能觉得网络编程是...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 138,362
精华内容 55,344
关键字:

网络编程参数错误要怎么解决