网络编程
一、JAVA网络编程–基础知识
1、概述
计算机网络是通过传输介质、通信设施和网络通信协议,把分散在不同地点的计算机设备互连起来的,实现资源共享和数据传输的系统。网络编程就是编写程序使互联网的两个(或多个)设备(如计算机)之间进行数据传输。Java语言对网络编程提供了良好的支持。通过其提供的接口我们可以很方便地进行网络编程。
2、网络分层
计算机网络20世纪60年代出现,经历了20世纪70年代、80年代和90年代的发展,进入21世纪后,计算机网络已经成为信息社会的基础设施,深入到人类社会的方方面面,与人们的工作、学习和生活息息相关。计算机网络分为网络协议和网络体系结构。
2.1、网络体系结构
通过网络发送数据是一项复杂的操作,必须仔细地协调网络的物理特性以及所发送数据的逻辑特征。通过网络将数据从一台主机发送到另外的主机,这个过程是通过计算机网络通信
来完成。
网络通信的不同方面被分解为多个层
,层与层之间用接口连接
。通信的双方具有相同的层次,层次实现的功能由协议数据单元(PDU)来描述
。不同系统中的同一层构成对等层,对等层之间通过对等层协议进行通信,理解批次定义好的规则和约定。每一层表示为物理硬件(即线缆和电流)与所传输信息之间的不同抽象层次。在理论上,每一层只与紧挨其上和其下的层对话。将网络分层,这样就可以修改甚至替换某一层的软件,只要层与层之间的接口保持不变,就不会影响到其他层。
计算机网络体系结构是计算机网络层次和协议的集合,网络体系结构对计算机网络实现的功能,以及网络协议、层次、接口和服务进行了描述,但并不涉及具体的实现。接口是同一节点内相邻层之间交换信息的连接处,也叫服务访问点(SAP)。
梳理总结:
- 计算机之间的网络通信被分解为多个层,层与层之间用
接口
连接。 - 通信的双方具有相同的层次,层次实现的功能由协议数据单元(PDU)来描述。
- 不同系统中的同一层构成
对等层
,对等层之间通过对等层协议进行通信。 - 在通信的时候数据必须由一层依次传递到下一层,不能跨级传输。
为了促进计算机网络的发展,国际标准化组织ISO在现有网络的基础上,提出了不基于具体机型、操作系统或公司的网络体系结构,称为开放系统互连参考模型,即OSI/RM。但是ISO制定的OSI参考模型过于庞大、复杂招致了许多批评。因此美国国防部提出了TCP/IP协议栈参考模型,简化了OSI参考模型,获得了广泛的应用。
世界上第一个网络体系结构由IBM公司提出(1974年,SNA),以后其他公司也相继提出自己的网络体系结构。为了促进计算机网络的发展,国际标准化组织ISO在现有网络的基础上,提出了不基于具体机型、操作系统或公司的网络体系结构,称为开放系统互连参考模型,即OSI/RM(Open System Interconnection Reference Model)。
ISO制定的OSI参考模型过于庞大、复杂招致了许多批评。与此相对,美国国防部提出了TCP/IP协议栈参考模型,简化了OSI参考模型,由于TCP/IP协议栈的简单,获得了广泛的应用,并成为后续因特网使用的参考模型。
2.1.1、OSI参考模型
这里首先介绍OSI参考模型,是国际标准化组织ISO提出的,把网络通信的工作分为7层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。也是我们在大学计算机网络课程中认识的那样。下面一张图来看看。
-
物理层处于OSI的最底层,是开放系统的基础,它的功能主要是为计算机提供传送数据的通路以及传输数据。
传输信息离不开物理介质,如双绞线和同轴电缆。物理层的任务就是为它的上一层提供物理连接,以及规定通行节点之间的机械和电器等特征。在这一层,数据作为原始的比特流(bit)传输。典型的设备包括集线器(Hub)。
-
数据链路层的主要任务是实现计算机网络中相邻节点之间的可靠传输,把原始的、有差错的物理传输数据加上数据链路协议以后,构成逻辑上可靠的数据链路。
数据链路层负责两个相邻的节点间的线路上,无差错的传送以帧(Frame)为单位的数据。每一帧包括一定量的数据和一些必要的控制信息。在传送数据时,如果接收方检测到所传的数据中有差错,就要通知发送方重新发送这一帧。本层的典型的设备是Switch(交换机)。
-
网络层主要完成的功能主要包括路由选择、网络寻址、流量控制、拥塞控制、网络互连等。实现两个计算机节点之间的数据传输。
在计算机网络中两台计算机之间可能会经过很多的数据链路,也可能通过很多的通信子网。网络层的任务就是选择合适的网间路由和交换节点,确保数据及时传送到目标主机。在这一层,数据的单位称为包(Packet)。网络层将数据链路层提供的帧组成数据包,包中封装了网络层的包头,包头中含有逻辑地址信息(源主机和目标主机的网络地址)。本层的典型的设备就是路由器(Router)。
-
传输层涉及源端节点到目的端节点之间可靠的信息传输。也就是说是采用TCP/IP还是采用UDP协议。他需要三次握手四次挥手。
该层的任务是通过通信子网的特性最好地利用网络资源,为两个源主机和目标主机的会话层提供建立、维护和取消传输的连接的功能,以可靠的方式或不可靠的方式。在这一层,以可靠方式传输的数据单位称为段(Segment),以不可靠方式传输的数据单位称为数据报(Datagram)。
-
会话层的主要功能是负责应用程序之间建立、维持和中断会话,提供单工、半双工和全双工3种不同的通信方式,使系统和服务之间有序地进行通信。
在会话层及以上层次中,数据传送的单位不再另外命名,统称为报文(Message)。会话层管理进程之间的会话过程,即负责建立、管理和终止进程之间的会话。会话层还通过数据中插入校验点来实现数据的同步。
-
表示层关心所传输数据信息的格式定义,其主要功能是把应用层提供的信息变换为能够共同理解的形式,提供字符代码、数据格式、控制信息格式、加密等的统一表示。
表示层对上层的数据进行转换,以保证一个主机的应用层的数据可以被另一个主机的应用层理解。表示层的数据转换包括对数据的加密、解密、压缩、解压和格式转换等。
-
应用层是直接为应用进程提供服务的。其作用是多个系统应用进程相互通信的同时,完成一系列业务处理所需的服务。
应用层确定进程之间通信的实际用途,以满足用户的实际请求。例如浏览Web站点、收发E-mail、上传或下载文件等。
在数据通信中,发送方每一层都将上层的数据加上一个报头,并交给下一层。这一过程重复进行,直到底层的物理层,然后通过物理链路实际传送到接收放。在接收方,则将报头层层剥离,最后将数据交给接收进程。
不同主机之间的相同层次称为对等层。例如主机A中的表示层和主机B中的表示层互为对等层、主机A中的会话层和主机B中的会话层互为对等层等。对等层之间互相通信需要遵守一定的规则,如通信的内容、通信的方式,我们将其称为协议(Protocol)。我们将某个主机上运行的某种协议的集合称为协议栈。主机正是利用这个协议栈来接收和发送数据的。
OSI参考模型通过将协议栈划分为不同的层次,可以简化问题的分析、处理过程以及网络系统设计的复杂性。
TCP/IP分层模型 ISO制定的OSI参考模型的过于庞大、复杂招致了许多批评。与此对照,由技术人员自己开发的TCP/IP协议栈获得了更为广泛的应用。实际上,TCP/IP协议也是目前因特网范围内运行的唯一一种协议。图表示了TCP/IP分层模型与OSI模型的对比图。
2.1.2、TCP/IP参考模型
TCP/IP,即Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,是Internet最基本的协议,Internet国际互联网络的基础。
TCP/IP协议是一个开放的网络协议簇
,它的名字主要取自最重要的网络层IP协议和传输层TCP协议。TCP/IP协议定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。TCP/IP参考模型采用4层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求,这4个层次分别是:网络接口层、网络层(IP层)、传输层(TCP层)、应用层。
-
网络接口层:
TCP/IP协议对网络接口层没有给出具体的描述,网络接口层对应着OSI参考模型的
物理层
和数据链路层
。 -
网络层(IP层):
网络层是整个TCP/IP协议栈的
核心
。它的功能是把分组发往目标网络或主机。同时,为了尽快地发送分组,可能需要沿不同的路径同时进行分组传递。因此,分组到达的顺序和发送的顺序可能不同,这就需要上层必须对分组进行排序。网络层除了需要完成路由
的功能外,也可以完成将不同类型的网络(异构网)互连的任务。除此之外,互联网层还需要完成拥塞控制的功能。 -
传输层(TCP层):
TCP层负责在应用进程之间建立端到端的连接和可靠通信,它只存在与端节点中。TCP层涉及两个协议,TCP和UDP。其中,TCP协议提供
面向连接
的服务,提供按字节流的有序、可靠传输,可以实现连接管理、差错控制、流量控制、拥塞控制等。UDP协议提供无连接
的服务,用于不需要或无法实现面向连接的网络应用中。 -
**应用层:**为各种网络应用提供服务。
应用层为Internet中的各种网络应用提供服务。
2.2、网络协议
如同人与人之间相互交流是需要遵循一定的规则(如语言)一样,计算机之间能够进行相互通信是因为它们都共同遵守一定的规则,即网络协议。
OSI参考模型和TCP/IP模型在不同的层次中有许多不同的网络协议,如图所示:
网络协议之间的关系图如下:
2.2.1、IP协议(Internet protocol)
IP协议的作用在于把各种数据包准备无误的传递给对方,其中两个重要的条件是IP地址
和MAC地址
。由于IP地址是稀有资源,不可能每个人都拥有一个IP地址,所以我们通常的IP地址是路由器给我们生成的IP地址,路由器里面会记录我们的MAC地址。而MAC地址是全球唯一的。举例,IP地址就如同是我们居住小区的地址,而MAC地址就是我们住的那栋楼那个房间那个人。IP地址采用的IPv4格式,目前正在向IPv6过渡。
2.2.2、TCP协议(Transmission Control Protocol)
TCP(传输控制协议)是面向连接的传输层协议。TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。TCP协议采用字节流传输数据。
2.2.2.1、TCP的报文格式
TCP报文段包括协议首部和数据两部分,协议首部的固定部分是20
个字节,首部的固定部分后面是选项部分。
下面是报文段首部各个字段的含义:
- 源端口号以及目的端口号:各占2个字节,端口是
传输层
和应用层
的服务接口,用于寻找发送端和接收端的进程,一般来讲,通过端口号和IP地址,可以唯一确定一个TCP连接,在网络编程中,通常被称为一个socket
接口。 - 序号:Seq序号,占4个字节、32位。用来标识从TCP发送端向TCP接收端发送的
数据字节流
。发起方发送数据时对此进行标记。 - 确认序号:Ack序号,占4个字节、32位。包含发送确认的一端所期望收到的下一个序号。只有ACK标记位为1时,确认序号字段才有效,因此,确认序号应该是上次已经成功收到数据字节序号加1,即Ack=Seq + 1。
- 数据偏移:占4个字节,用于指出TCP首部长度,若不存在选项,则这个值为20字节,数据偏移的最大值为60字节。
- 保留字段: 占6位,暂时可忽略,值全为0。
- 标志位,6个
- URG(紧急):为1时表明紧急指针字段有效。
- ACK(确认):为1时表明确认号字段有效。
- PSH(推送):为1时接收方应尽快将这个报文段交给应用层。
- RST(复位):为1时表明TCP连接出现故障必须重建连接。
- SYN(同步):在连接建立时用来同步序号。
- FIN(终止):为1时表明发送端数据发送完毕要求释放连接。
- **接收窗口:**占2个字节,用于流量控制和拥塞控制,表示当前接收
缓冲区
的大小。在计算机网络中,通常是用接收方的接收能力的大小来控制发送方的数据发送量。TCP连接的一端根据缓冲区大小确定自己的接收窗口值,告诉对方,使对方可以确定发送数据的字节数
。 - **校验和:**占2个字节,范围包括首部和数据两部分。
- 选项是可选的,默认情况是不选。
2.2.2.2、三次握手与四次挥手
TCP是面向连接的协议,因此每个TCP连接
都有3个阶段:连接建立、数据传送和连接释放。连接建立经历三个步骤,通常称为“三次握手”。
TCP三次握手过程如下:
-
第一次握手(客户端发送请求)
客户机发送连接请求报文段到服务器,并进入SYN_SENT状态,等待服务器确认。发送连接请求报文段内容:
SYN=1,seq=x;SYN=1
意思是一个TCP的SYN标志位置为1的包,指明客户端打算连接的服务器的端口;seq=x表示客户端初始序号x,保存在包头的序列号(Sequence Number)字段里。 -
第二次握手(服务端回传确认)
服务器收到客户端连接请求报文,如果同意建立连接,向客户机发回确认报文段(ACK)应答,并为该TCP连接分配TCP缓存和变量。服务器发回确认报文段内容:
SYN=1,ACK=1,seq=y,ack=x+1
;SYN标志位和ACK标志位均为1,同时将确认序号(Acknowledgement Number)设置为客户的ISN加1,即x+1;seq=y为服务端初始序号y。 -
第三次握手(客户端回传确认)
客户机收到服务器的确认报文段后,向服务器给出确认报文段(ACK),并且也要给该连接分配缓存和变量。此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。客户端发回确认报文段内容:ACK=1,seq=x+1,ack=y+1;ACK=1为确认报文段;seq=x+1为客户端序号加1;ack=y+1,为服务器发来的ACK的初始序号字段+1。
注意:握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。
TCP四次挥手过程如下:
由于TCP连接是全双工
的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
- TCP客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入FIN_WAIT_1状态。发送报文段内容:FIN=1,seq=u;FIN=1表示请求切断连接;seq=u为客户端请求初始序号。
- 服务端收到这个FIN,它发回一个ACK给客户端,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号;服务端进入CLOSE_WAIT状态。发送报文段内容:ACK=1,seq=v,ack=u+1;ACK=1为确认报文;seq=v为服务器确认初始序号;ack=u+1为客户端初始序号加1。
- **服务器关闭客户端的连接后,发送一个FIN给客户端,**服务端进入LAST_ACK状态。发送报文段内容:FIN=1,ACK=1,seq=w,ack=u+1;FIN=1为请求切断连接,ACK=1为确认报文,seq=w为服务端请求切断初始序号。
- 客户端收到FIN后,客户端进入TIME_WAIT状态,接着发回一个ACK报文给服务端确认,并将确认序号设置为收到序号加1,服务端进入CLOSED状态,完成四次挥手。发送报文内容:ACK=1,seq=u+1,ack=w+1;ACK=1为确认报文,seq=u+1为客户端初始序号加1,ack=w+1为服务器初始序号加1。
注意:为什么连接的时候是三次握手,关闭的时候却是四次挥手?
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭socket,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文,我收到了”。只有等到服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送,故需要四步挥手。
2.2.3、UDP协议(User Datagram Protocol)
UDP,用户数据报协议,它是TCP/IP
协议簇中无连接的运输层协议。
- UDP是一个非连接的协议,传输数据之前源端和终端
不建立连接
,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。 - 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务器可同时向多个客户端传输相同的消息。
- UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小。
- 吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制。
- UDP使用尽量最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态表。
- UDP是面向报文的。发送方的UDP对应用程序交下来的报文,在添加首部受就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。
2.2.3.1、UDP协议格式
UDP协议由两部分组成:首部和数据。其中,首部仅有8
个字节,包括源端口和目的端口、长度(UDP用于数据报的长度)、校验和。
2.2.3.2 TCP与UDP的区别
- TCP基于连接,UDP是无连接的;
- 对系统资源的要求,TCP较多,UDP较少;
- UDP程序结构较简单;
- TCP是流模式,而UDP是数据报模式;
- TCP保证数据正确性,而UDP可能丢包;TCP保证数据顺序,而UDP不保证;
2.2.4 HTTP协议(Hypertext Transfer Protocol)
HTTP,超文本传输协议,它是互联网上应用最为广泛的一种网络协议。HTTP是一种应用层协议,它是基于TCP协议之上的请求/响应式
的协议。HTTP协议是Web浏览器和Web服务器之间通信的标准协议。HTTP指定客户端与服务器如何建立连接、客户端如何从服务器请求数据,服务器如何响应请求,以及最后如何关闭连接。HTTP连接使用TCP/IP来传输数据。
对于从客户端到服务器的每一个请求,都有4个步骤:
- 默认情况下,客户端在端口 80 打开与服务器的一个TCP连接,URL中还可以指定其他端口。
- 客户端向服务器发送消息,请求指定路径上的资源。这个资源包括一个首部,可选地(取决于请求的性质)还可以有一个
空行
,后面是这个请求的数据
。 - 服务器向客户端发送响应。响应以响应码开头,后面是包含数据的
首部
、一个空行以及所请求的文档或错误消息。 - 服务器
关闭连接
。
现在使用的HTTP协议是HTTP/1.1版本,1997年之前采用的是HTTP1.0版本。HTTP连接在1.0版本中采用非持续连接工作方式,1.1版本采用的是持续连接工作方式,持续连接是指服务器在发送响应后仍然在一段时间内保持这条由TCP运输层协议建立起来的连接,使客户端和服务器可以继续在这条连接上传输HTTP报文。
是否采用持续连接工作方式,1.0中默认是关闭的,需要在HTTP头加入“Connection:Keep-Alive”,才能启用Keep-Alive。HTTP1.1中默认启用Keep-Alive,如果加入“Connection:close”,才关闭。目前大部分浏览器都是用HTTP1.1协议,也就是说默认都会发起Keep-Alive的连接请求了,所以是否能完成一个完整的Keep-Alive连接就看服务器设置情况。
2.2.4.1 HTTP报文
HTTP协议是基于TCP协议之上的请求/响应
式协议,下面主要介绍HTTP报文的格式,HTTP报文主要有请求报文和响应报文两种。
首先看HTTP请求报文的格式:
HTTP请求报文由请求行、首部行和实体主体组成,由浏览器发送给服务器。上面这张图中SP表示空格,cr lf表示回车和换行。下图是谷歌浏览器内访问服务器查看的HTTP请求例子:
HTTP响应报文格式:
上面这张图是HTTP响应报文,它由状态行、首部行和实体主体组成。下图为HTTP响应报文例子:
2.2.4.2 HTTP请求方法和响应状态码
在上面的HTTP请求报文例子中,我们可以看到请求方法是GET,这表示请求读取由URL所标志的信息,除了GET,还有其他几种常用的方法。
在HTTP响应报文的例子中,我们可以看到状态码是200,表示响应成功。下表是其他状态码,总共5大类,33种。
2.2.4.3 HTTP和HTTPS的区别
HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单来说就是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。它是一个URL scheme(抽象标识符体系),句法类同http:体系,用于安全的HTTP数据传输。https:URL表明它使用了HTTP,但HTTPS存在不同于HTTP的默认端口及一个加密/身份验证层(在HTTP与TCP之间)。
超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息。HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此HTTP协议不适合传输一些敏感信息,比如信用开号、密码等。
为了解决HTTP协议的这一缺陷,需要使用另一种协议:安全套接字层超文本传输协议HTTPS。为了数据传输的安全,HTTPS在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。
HTTPS和HTTP的区别主要为以下四点:
- https协议需要到ca申请证书,一般免费证书很少,需要缴费。
- http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
- http和https使用的是完全不同的连接方式,用的端口也不一样,前者是
80
,后者是443
。 - http的连接很简单,是无状态的;https协议是有ssl+http协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
2.2.4.4 HTTP和TCP/IP协议的关系
网络中有一段比较容易理解的介绍:
“我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也 可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP文本信息,然后使用TCP/IP做传输层协议将它发到网络上。”
3. Java Socket网络编程
3.1、Socket概述
Java的网络编程主要涉及到的内容是Socket编程。Socket套接字
,就是两台主机之间逻辑连接的端点。TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。Socket是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议
、本地主机的IP地址
、本地进程的协议端口
、远程主机的IP地址
、远程进程的协议端口
。
应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket
)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
Socket,实际上是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。实际上,Socket跟TCP/IP协议没有必然的关系,Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket的出现,只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、listen、accept、send、read和write等等。网络有一段关于socket和TCP/IP协议关系的说法比较容易理解:
“TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。这个就像操作系统会提供标准的编程接口,比如win32编程接口一样,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。”
实际上,传输层的TCP是基于网络层的IP协议的,而应用层的HTTP协议又是基于传输层的TCP协议的,而Socket本身不算是协议,就像上面所说,它只是提供了一个针对TCP或者UDP编程的接口。socket是对端口通信开发的工具,它要更底层一些。
3.2、Socket整体流程
Socket编程主要涉及到客户端和服务端两个方面,首先是在服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到65536,但是0到1024是为特权服务保留的端口号,我们可以选择任意一个当前没有被其他进程使用的端口。
客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。
实例一
下面是一个客户端和服务器端进行数据交互的简单例子,客户端输入正方形的边长,服务器端接收到后计算面积并返回给客户端,通过这个例子可以初步对Socket编程有个把握。
-
服务器端
public class SocketServer { public static void main(String[] args) throws IOException { // 端口号 int port = 7000; // 在端口上创建一个服务器套接字 ServerSocket serverSocket = new ServerSocket(port); // 监听来自客户端的连接 Socket socket = serverSocket.accept(); // 输入流 DataInputStream dis = new DataInputStream( new BufferedInputStream(socket.getInputStream())); // 输出流 DataOutputStream dos = new DataOutputStream( new BufferedOutputStream(socket.getOutputStream())); do { // 读取客户端接收的边长 double length = dis.readDouble(); System.out.println("服务器端收到的边长数据为:" + length); double result = length * length; // 发送计算的面积 dos.writeDouble(result); dos.flush(); } while (dis.readInt() != 0);// 继续接收 or 关闭资源 socket.close(); serverSocket.close(); } }
-
客户端
public class SocketClient { public static void main(String[] args) throws UnknownHostException, IOException { // 端口 int port = 7000; // 地址 String host = "localhost"; // 创建一个套接字并将其连接到指定端口号 Socket socket = new Socket(host, port); // 输入流 DataInputStream dis = new DataInputStream( new BufferedInputStream(socket.getInputStream())); // 输出流 DataOutputStream dos = new DataOutputStream( new BufferedOutputStream(socket.getOutputStream())); Scanner sc = new Scanner(System.in);// 屏幕输入 boolean flag = false; while (!flag) { System.out.println("请输入正方形的边长:"); double length = sc.nextDouble(); // 发送长度值 dos.writeDouble(length); dos.flush(); // 读取服务器端的计算结果 double area = dis.readDouble(); System.out.println("服务器返回的计算面积为:" + area); while (true) { System.out.println("继续计算?(Y/N)"); String str = sc.next(); if (str.equalsIgnoreCase("N")) {// 不考虑大小写 dos.writeInt(0);// 停止通信 dos.flush(); flag = true; break; } else if (str.equalsIgnoreCase("Y")) { dos.writeInt(1);// 继续通信 dos.flush(); break; } } } socket.close(); } }
实例二
可以看到上面的服务器端程序和客户端程序是一对一的关系,为了能让一个服务器端程序能同时为多个客户提供服务,可以使用多线程机制,每个客户端的请求都由一个独立的线程进行处理。下面是改写后的服务器端程序。
package socket;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SocketServerM {
public static void main(String[] args) throws IOException {
// 端口
int port = 7000;
// 服务器编号
int clientNo = 1;
ServerSocket serverSocket = new ServerSocket(port);
// 创建线程池
ExecutorService exec = Executors.newCachedThreadPool();
try {
while (true) {
Socket socket = serverSocket.accept();
exec.execute(new SingleServer(socket, clientNo));// 开启新线程
clientNo++;
}
} finally {
serverSocket.close();
}
}
}
// SingleServer 服务器端业务
class SingleServer implements Runnable {
private Socket socket;// Socket对象
private int clientNo;// clientNo客户端编号
public SingleServer(Socket socket, int clientNo) {
this.socket = socket;
this.clientNo = clientNo;
}
@Override
public void run() {
try {
// 输入流
DataInputStream dis = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
// 输出流
DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(socket.getOutputStream()));
do {
double length = dis.readDouble();
System.out.println("从客户端" + clientNo + "接收到的边长数据为:" + length);
double result = length * length;
dos.writeDouble(result);
dos.flush();
} while (dis.readInt() != 0);
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("与客户端" + clientNo + "通信结束");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
上面改进后的服务器端代码可以支持不断地并发响应网络中的客户请求。关键的地方在于多线程机制的运用,同时利用线程池可以改善服务器程序的性能。
二、Socket
当客户端建立一个到服务器的套接字
连接时,客户端需要指定一个端口号
。该端口号指示服务器正在监听的端口。但是,在客户端和服务器使用套接字连接后,它们的连接实际上发生在不同的端口。这就允许在分离线程上的服务器继续在其端口上监听其它客户端。这一切都是在后台发生的,不影响我们的代码。
在连接建立后,通讯可以通过使用I/O流发生。每个套接字都有一个OutputStream
和一个InputStream
。客户端的OutputStream连接到服务器的InputStream,同时客户端的InputStream连接到服务器的OutputStream。TCP是一个双向的通讯协议,所以数据可以同时通过两个流发生。
套接字流是低级I/O流InputStream
和OutputStream
。因此,它们可以与缓冲流、过滤流以及其它高级流链接在一起,从而可以执行任何类型的高级I/O。这也是为什么我们要在网络编程之前学习java.io包的原因。我们将会发现建立连接是很简单的,网络编程的大部分工作是将数据传过来传过去。当然,这是网络编程应该做的,从而让我们将注意力放在要解决的问题上,而不用关心低层通讯以及协议详细。这也是为什么Java在网络编程中如此流行的原因之一。
下面,我们来看一个使用套接字的示例。我们将从一个运行在服务器上的程序开始,让它为客户端请求监听某个端口。然后我们再展示如何编写一个连接到服务器应用的客户端代码。
1、ServerSocket类
java.net.ServerSocket类用于服务器程序获得一个端口,并监听客户端请求。该类有四个构造器:
// 创建绑定到特定端口的服务器套接字。如果该端口已经被其它应用程序绑定,那么就会发生一个异常。如果端口号设置为0,将会在任何空闲的端口上创建套接字。
public ServerSocket(int port) throws IOException
// 利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号。backlog参数指定可以有多少客户端存在等待队列中。如果队列满了,当客户端试图连接到该端口时,就会接收到一个异常。如果该值为0,就使用本地平台默认的队列大小。
public ServerSocket(int port, int backlog) throws IOException;
// 与上一个构造器相似,但是用InetAddress参数指定要绑定到的本地IP地址。有多个IP地址的服务器可以用InetAddress来指定用哪个IP地址来接收客户端请求。
public ServerSocket(int port, int backlog, InetAddress address) throws IOException
// 创建非绑定服务器套接字。在使用该构造器时,用bind()方法来只绑定服务器套接字。
public ServerSocket() throws IOException
如果发生了错误,那么上述每个构造器都会抛出一个IOException异常。如果ServerSocket构造器没有抛出异常,这就意味着程序已经成功绑定到指定端口,并且为客户端请求做好了准备。ServerSocket类的常用方法如下:
// 返回服务器套接字监听的端口。如果我们给ServerSocket的构造器传入端口号为0,让服务器选择一个端口,那么就要用到这个方法。
public int getLocalPort()
// 等待要连接的客户端。该方法会阻塞,直到一个客户端连接到指定端口上的服务器,或者假如我们用setSoTimeout()方法设置了超时值,而套接字超过该值了。否则,该方法就无限阻塞下去。
public Socket accept() throws IOException
// 设置超时值,指定在accept()方法调用期间,服务器套接字等待客户端的时间。
public void setSoTimeout(int timeout)
// 将套接字绑定到SocketAddress对象中指定的服务器和端口。如果我们使用无参数的构造器实例化一个ServerSocket对象,就要使用该方法。
public void bind(SocketAddress host, int backlog)
以上几个方法中,accept
()方法是我们要关注的重点,因为这是服务器如何监听到来的请求的方法。当ServerSocket调用accept
()方法时,如果没有设定超时值,该方法就一直要等到客户端连接后才返回。在客户端连接上来以后,ServerSocket就在一个非指定的端口上(这个端口与它监听的端口不同)创建一个新的Socket,并返回一个对这个新Socket的引用。现在TCP连接就在客户端和服务器之间存在了,通讯就开始了。
注意:如果编写一个允许多个客户端的服务器程序,那么我们肯定想让服务器套接字持续调用accept()来等待客户端。标准的技巧是当客户端连接时,启动一个新线程,用于与新客户端通讯,而让当前线程马上再次调用accept()方法。例如,如果有50个客户端连接到一个服务器,那么服务器端就会有51个线程:50个用于与客户端通讯,1个用于通过accept()方法等待新的客户端。
2、Socket类
java.net.Socket类代表用于客户端和服务器相互通讯的套接字。客户端通过实例化一个Socket
获得Socket
对象,而服务器从ServerSocket对象的accept
()方法的返回值中获得一个Socket对象。Socket类有五个构造器用于将客户端连接到服务器:
// 试图连接到服务器上指定端口。如果该构造器没有抛出一个异常,那么连接就成功了,而客户端就连接到服务器了。当连接到服务器时,这是最简单的构造器。
public Socket(String host, int port) throws UnknownHostException, IOException
// 与上一个构造器相同,但是主机是通过InetAddress对象描述的。
public Socket(InetAddress host, int port) throws IOException
// 连接到指定的主机和端口,并在本地主机上的指定地址和端口上创建一个套接字。当客户端有多个IP地址,或者想让套接字绑定到特定的本地端口上时,通常使用该构造器。
public Socket(String host, int port, InetAddress localAddress, int localPort) throws IOException
// 与前一个构造器相同,但是主机是用InetAddress描述的。
public Socket(InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException
// 创建一个非连接的套接字。以后可以使用connect()方法将该套接字连接到服务器。
public Socket()
当Socket类的构造器返回时,它不仅仅是实例化一个Socket对象。在构造器中,它实际上试图连接到指定的服务器和端口。如果构造器成功返回,客户端就有了一个到服务器的TCP连接。
下面列出了Socket类的一些方法。注意,因为客户端和服务器都有一个Socket对象,所以客户端和服务器都可以调用这些方法。
// 将套接字连接到指定主机。本方法只有在使用无参数构造器实例化Socket时才需要。
public void connect(SocketAddress host, int timeout) throws IOException
// 返回该Socket连接到的其它计算机的地址。
public InetAddress getInetAddress()
// 返回套接字绑定到的远程机器上的端口。
public int getPort()
// 返回套接字绑定到的本地机器上的端口。
public int getLocalPort()
// 返回远程套接字的地址。
public SocketAddress getRemoteSocketAddress()
// 返回套接字的输入流。该输入流连接到远程套接字的输出流。
public InputStream getInputStream() throws IOException
// 返回套接字的输出流。该输出流连接到远程套接字的输入流。
public OutputStream getOutputStream() throws IOException
// 关闭套接字,让该Socket对象不再有能力再次连接到任何服务器。
public void close() throws IOException
Socket类包含了很多方法,完整的方法列表请参考JDK文档。我们会注意到Socket类中的很多方法包含允许访问和修改一个连接的不同TCP属性,例如超时值或保持活动状态设置。在所有的Socket类的方法中,最重要的两个方法是getInputStream()和getOutputStream()。下面我们将进行详细讨论。
UDP套接字编程
用户报文协议(UDP)是用于将二进制数据从一台计算机发送到另一台计算的非连接协议。这里,数据被称为数据报包,它包含了数据将要发送到的目标服务器和端口号。消息的发送者使用数据报套接字发送包,接受者使用数据报套接字接收消息。
当消息被发送时,接受者并不需要是可用的。同样,当消息接收时,发送者也不需要是可用的。
3、DatagramSocket类
数据报包的发送者和接收者都使用java.net.DatagramSocket类分别发送和接收包。DatagramSocket类有四个构造器:
// 创建数据报套接字,并将其绑定到本地主机指定的端口上。
public DatagramSocket(int port) throws SocketException
// 使用指定的端口和本地地址创建数据报套接字。如果计算机有多个地址,就应该使用本构造器。
public DatagramSocket(int port, InetAddress address) throws SocketException
// 在指定的SocketAddress上创建数据报套接字。SocketAddress封装了服务器名和端口号。
public DatagramSocket(SocketAddress address) throws SocketException
// 创建一个非绑定的数据报套接字。创建之后,可以使用DatagramSocket类的bind()方法将该套接字绑定到一个端口。
public DatagramSocket() throws SocketException
如下是DatagramSocket类的一些方法:
// 发送指定的数据报包。DatagramPacket对象包含了包的目的地信息。
public void send(DatagramPacket packet) throws IOException
// 接受一个数据报包,将其存在指定的参数上。该方法会一直阻塞,不返回,除非接收到数据报包,或者套接字超时。如果套接字超时,就抛出一个SocketTimeoutException异常。
public void receive(DatagramPacket packet) throws IOException
// 设置套接字的超时值。这个超时值决定了receive()方法阻塞的微秒数。
public void setSoTimeout(int timeout) throws SocketTimeoutException
4、DatagramPacket类
DatagramSocket类的send
()和receive
()方法都带有一个DatagramPacket参数。DatagramPacket类代表一个数据报包,与DatagramSocket类似,包的发送者和接受者都要使用它。DatagramPacket有六个构造器。其中,两个是用于接受者,四个用于发送者。
如下的两个DatagramPacket构造器用于接收数据报包:
// 创建一个数据报包,用于接收指定大小的包。buffer包含了要接收的包。
public DatagramPacket(byte [] buffer, int length)
// 与上一个构造器相同,除了要接收的包的数据存在用offset参数指定的字节数字位置上。
public DatagramPacket(byte [] buffer, int offset, int length)
传递给这些构造器的字节数组用于存储要接收的包的数据,通常是空数组。如果它们不会空,那么要接收的数据报包将覆盖数组中的数据。
如下的四个构造器用于发送一个数据报包:
// 创建一个数据报包,用于发送指定大小的包。buffer用于存储包数据,address和port描述接收者。
public DatagramPacket(byte [] buffer, int length, InetAddress address, int port)
// 与上一个构造器相似,但是接收者的名字和端口号存储在SocketAddress参数中。
public DatagramPacket(byte [] buffer, int length, SocketAddress address)
// 创建一个数据报包,用来将长度为 length 偏移量为 offset 的包发送到指定主机上的指定端口号。
public DatagramPacket(byte [] buffer, int offset, int length, InetAddress address, int port)
//与前一个构造器类似,但是接收者的名字和端口号包含在SocketAddress参数中。
public DatagramPacket(byte [] buffer, int offset, int length, SocketAddress address)
注意六个构造器都带有一个字节数组做参数。当接收包时,该数组开始是空,然后被接收的数据报包填充。当发送包时,字节数组存储要发送的包的数据。
DatagramPacket类包含了数据报包不同属性的get和set方法:
// 返回数据缓冲区。
public byte [] getData()
// 设置包数据。
public void setData(byte [] buffer)
// 返回将要发送或接收到的数据的长度。
public int getLength()
// 设置将要发送或者接收到的数据的长度。
public void setLength(int length)
// 返回消息要发送到的或者发出此消息的远程主机的地址。
public SocketAddress getSocketAddress()
// 设置消息要发送到的或者发出消息的远程主机的地址。
public void setSocketAddress(SocketAddress address)
5、URL类与URLConnection类
已经讨论了在Java中如何使用套接字和数据报包来创建网络应用。在本节中,我们将学习如何编写与URL资源进行通讯的Java程序。URL(Uniform Resource Locator)代表统一资源定位,代表万维网上的一个资源,例如一个网页或者一个FTP目录。
一个URL实际上是一类URI(Uniform Resource Identifier,统一资源标识符)。URI标识一个资源,但是不包括如何访问该资源的信息。URL标识一个资源以及访问该资源的协议。URI在java中使用java.net.URI类代表。一个URL可以分为如下几个部分:
协议://主机:端口/路径?查询字符串#锚点引用;这里,路径也称为文件名,主机也称为授权。协议包含HTTP、HTTPS、FTP和File。
java.net.URL类代表一个URL。URL类有如下几个构造器用于创建URL:
// MalformedURLException:根据指定 protocol、host、port 号和 file 创建 URL 对象。
public URL(String protocol, String host, int port, String file) throws
// 与前一构造器相同,但是用的是指定协议的默认端口。
public URL(String protocol, String host, String file) throws MalformedURLException
// 根据给定的字符串创建URL对象。
public URL(String url) throws MalformedURLException
URL类中有很多方法用于访问URL的不同部分,常用的方法如下:
public String getPath()// 获取此URL的路径部分。
public String getQuery()// 获取此 URL 的查询部分。
public String getAuthority()// 获取此 URL 的授权部分。
public int getPort()// 获取此 URL 的端口号。
public int getDefaultPort()// 获取与此 URL 关联协议的默认端口号。
public String getProtocol()// 获取此 URL 的协议名称。
public String getHost()// 获取此 URL 的主机名。
public String getFile()// 获取此 URL 的文件名。
public String getRef()// 获取此 URL 的锚点。
public URLConnection openConnection() throws IOException// 打开一个到该URL的连接,允许客户端与该资源进行通讯。
使用URL类的openConnection()方法,可以连接到一个URL,并与资源进行通讯。openConnection()方法返回一个java.net.URLConnection,URLConnection是一个抽象类,其子类代表不同类型的URLConnection。例如,如果我们连接到一个HTTP协议的URL,那么openConnection()方法就返回一个HttpURLConnection对象。如果连接到一个代表JAR额外年间的URL,那么openConnection()方法就返回一个JarURLConnection对象。
URLConnection类有多个用于设置或者判断连接信息的方法,包括:
// 将此 URLConnection 的 doInput 字段的值设置为指定的值。URL 连接可用于输入和/或输出。如果打算使用 URL 连接进行输入,则将 DoInput 标志设置为 true;如果不打算使用,则设置为 false。默认值为 true,因为客户端通常是从URLConnection中读取。
public void setDoInput(boolean input)
// 将此 URLConnection 的 doOutput 字段的值设置为指定的值。如果打算使用 URL 连接进行输出,则将 DoOutput 标志设置为 true;如果不打算使用,则设置为 false。默认值为 false,因为很多URL类型不支持写入数据。
public void setDoOutput(boolean output)
// 返回从资源读取的URLConnection的输入流。
public InputStream getInputStream() throws IOException
// 返回写入到此连接的输出流。返回写入到资源的URLConnection的输出流。
public OutputStream getOutputStream() throws IOException
// 返回此URLConnection对象要连接到的URL。
public URLgetURL()
URLConnection类还包含了一些访问连接头信息的方法,让我们可以判断URL内容的类型和长度、最后更改的日子、内容编码等等。更多的方法,请参考JDK文档中URLConnection的详细说明。
三、IP
IP地址由四段组成,每个字段是一个字节,8位,最大值是255,,
IP地址由两部分组成,即网络地址和主机地址。网络地址表示其属于互联网的哪一个网络,主机地址表示其属于该网络中的哪一台主机。二者是主从关系。
IP地址的四大类型标识的是网络中的某台主机。IPv4的地址长度为32位,共4个字节,但实际中我们用点分十进制记法。
IP地址根据网络号和主机号来分,分为A、B、C三类及特殊地址D、E。 全0和全1的都保留不用。
A类:(1.0.0.0-126.0.0.0)(默认子网掩码:255.0.0.0或 0xFF000000)第一个字节为网络号,后三个字节为主机号。该类IP地址的最前面为“0”,所以地址的网络号取值于1~126之间。一般用于大型网络。
B类:(128.0.0.0-191.255.0.0)(默认子网掩码:255.255.0.0或0xFFFF0000)前两个字节为网络号,后两个字节为主机号。该类IP地址的最前面为“10”,所以地址的网络号取值于128~191之间。一般用于中等规模网络。
C类:(192.0.0.0-223.255.255.0)(子网掩码:255.255.255.0或 0xFFFFFF00)前三个字节为网络号,最后一个字节为主机号。该类IP地址的最前面为“110”,所以地址的网络号取值于192~223之间。一般用于小型网络。
D类:是多播地址。该类IP地址的最前面为“1110”,所以地址的网络号取值于224~239之间。一般用于多路广播用户[1] 。
E类:是保留地址。该类IP地址的最前面为“1111”,所以地址的网络号取值于240~255之间。
在IP地址3种主要类型里,各保留了3个区域作为私有地址,其地址范围如下:
A类地址:10.0.0.0~10.255.255.255
B类地址:172.16.0.0~172.31.255.255
C类地址:192.168.0.0~192.168.255.255
回送地址:127.0.0.1。 也是本机地址,等效于localhost或本机IP。一般用于测试使用。例如:ping 127.0.0.1来测试本机TCP/IP是否正常。
四、实用
1、UDP
java udp涉及到的类主要有DatagramSocket、DatagramPacket。
DatagramSocket()的主要方法有:
public synchronized
void receive(DatagramPacket p) throws IOException{…}
从此套接字接收数据包,数据报包DatapramPacket 缓冲区填充了接收的数据,数据报包还包含发送方的IP地址和发送机器上的端口号。此方法在未收到数据报包前会一直阻塞。数据报包对象的length
字段包含所接收信息的长度。如果信息比包的长度长,该信息将被截短。
public void send(DatagramPacket p) throws IOException {…}
从此套接字发送数据报包。``DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。
DatagramPacket :
此类表示数据报包。数据报包用来实现无连接包投递服务。每条报文仅根据该包中包含的信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。不对包投递做出保证。
其主要构造方法有:
主要方法有:
一个UDP Java Demo:
服务端:
public class UdpRecv {
public static void main(String[] args) throws Exception{
// 创建接收端Socket, 绑定本机IP地址, 绑定指定端口
DatagramSocket socket = new DatagramSocket(8080);
// 创建接收端Packet, 用来接收数据
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
// 用Socket接收Packet, 未收到数据时会阻塞
socket.receive(packet);
// 关闭Socket
socket.close();
// 从Packet中获取数据
byte[] data = packet.getData();
int len = packet.getLength();
String s = new String(data, 0, len, "UTF-8");
System.out.println(s);
}
}
客户端:
public class UdpSend {
public static void main(String[] args) throws Exception{
String s = "测试UDP!";
// 创建发送端Socket, 绑定本机IP地址, 绑定任意一个未使用的端口号
DatagramSocket socket = new DatagramSocket();
// 创建发送端Packet, 指定数据, 长度, 地址, 端口号
DatagramPacket packet = new DatagramPacket(s.getBytes("UTF-8"), s.getBytes().length, InetAddress.getByName("127.0.0.1"), 8080);
// 使用Socket发送Packet
socket.send(packet);
// 关闭Socket
socket.close();
}
}
2、TCP
Java实现TCP数据传输涉及到的类有Socket、ServerSocket。由此可看出TCP分客户端服务端,而UDP不分客户端服务端。Socket客户端服务端的读写是有先后顺序的,建立连接之后,客户端需要先向服务端写数据,写完之后需要调用socket.shutdownOutput()方法告诉服务端已经写完,这样服务端read()才会返回-1,客户端写完数据再读服务端返回的数据;而服务端是先读客户端传输过来的数据,再写需要向客户端传输的数据。如果是客户端先读服务端返回的数据再写向服务端发送的数据,这时服务端始终会先执行读客户端传输过来的数据,这时客户端却还没写,读的数据就会是空的。下面用代码演示:
2.1、服务端先写再读,客户端先读再写,服务端读的数据是空的
服务端:
public class TcpRecv {
public static void main(String[] args) throws Exception {
System.out.println("服务端控制台输出");
// 创建服务端serverSocket
ServerSocket serverSocket = new ServerSocket(8080);
// 服务端等待连接,如果未收到请求将一直阻塞
Socket socket = serverSocket.accept();
// 从socket中获取输出流,向客户端写数据
OutputStream outputStream = socket.getOutputStream();
// 从socket中获取输入流,读取客户端写的数据
InputStream inputStream = socket.getInputStream();
String s = "服务端返回给客户端的数据,服务端返回数据时间:" + System.nanoTime();
// 服务端先写,往客户端返回数据
outputStream.write(s.getBytes());
socket.shutdownInput();
// 服务端后读客户端传输过来的数据
byte[] buf = new byte[1024];
int len = 0;
StringBuffer sb = new StringBuffer();
while ((len = inputStream.read(buf)) != -1) {
System.out.println("服务端读数据");
sb.append(new String(buf, 0, len));
}
//传输过来的数据为空
System.out.println("在时间点为" + System.nanoTime() + "时服务端接收客户端的数据是【" + sb + "】");
inputStream.close();
serverSocket.close();
}
}
客户端:
public class TcpSend {
public static void main(String[] args) throws Exception {
System.out.println("客户端控制台输出");
// 创建一个未连接的socket连接
Socket socket = new Socket();
SocketAddress sa = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 8080);
// 建立连接
socket.connect(sa);
// 创建一个连接到端口为8091、地址为127.0.0.1的连接,如果服务端未开启会抛出异常
//socket = new Socket("127.0.0.1", 8091);
// 从socket中获取输入流,读取服务端返回的数据
InputStream inputStream = socket.getInputStream();
// 从socket中获取输出流,向服务端写数据
OutputStream outputStream = socket.getOutputStream();
// 客户端先读服务端返回的数据
byte[] buf = new byte[1024];
int len = 0;
StringBuffer sb = new StringBuffer();
while ((len = inputStream.read(buf)) != -1) {
sb.append(new String(buf, 0, len));
}
System.out.println("在时间点为" + System.nanoTime() + "时客户端读到服务端返回的数据【" + sb + "】");
// 客户端后向服务端写传输的数据
String s = "我是客户端发来的数据" + System.nanoTime();
System.out.println(s);
outputStream.write(s.getBytes());
socket.shutdownOutput();
socket.close();
System.out.println("客户端已关闭");
}
}
2.2、客户端先写再读,服务端先读再写
服务端:
public class TcpRecv {
public static void main(String[] args) throws Exception {
System.out.println("服务端控制台输出");
// 创建服务端serverSocket
ServerSocket serverSocket = new ServerSocket(8080);
// 服务端等待连接,如果未收到请求将一直阻塞
Socket socket = serverSocket.accept();
// 从socket中获取输出流,向客户端写数据
OutputStream outputStream = socket.getOutputStream();
// 从socket中获取输入流,读取客户端写的数据
InputStream inputStream = socket.getInputStream();
// 服务端读客户端传输过来的数据
byte[] buf = new byte[1024];
int len=0;
StringBuffer sb = new StringBuffer();
while((len=inputStream.read(buf))!=-1){
sb.append(new String(buf,0,len));
}
System.out.println("在时间点为" + System.nanoTime() + "时服务端接收客户端的数据是【"+sb+"】");
String s = "服务端返回给客户端的数据,服务端返回数据时间:" + System.nanoTime();
// 服务端向客户端写数据
outputStream.write(s.getBytes());
// 写完之后必须调用outputStream.close()或者shutdownOutput()
//socket.shutdownOutput();
outputStream.close();
serverSocket.close();
}
}
客户端:
public class TcpSend {
public static void main(String[] args) throws Exception {
System.out.println("客户端控制台输出");
// 创建一个未连接的socket连接
Socket socket = new Socket();
SocketAddress sa = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 8080);
// 建立连接
socket.connect(sa);
// 创建一个连接到端口为8091、地址为127.0.0.1的连接,如果服务端未开启会抛出异常
//socket = new Socket("127.0.0.1", 8091);
// 从socket中获取输入流,读取服务端返回的数据
InputStream inputStream = socket.getInputStream();
// 从socket中获取输出流,向服务端写数据
OutputStream outputStream = socket.getOutputStream();
// 客户端向服务端写数据
String s = "我是客户端发来的数据"+ System.nanoTime();
outputStream.write(s.getBytes());
// 必须要关闭socket的输出流,告诉对方已经写完,对方用read读才能读到-1,
// 如果用outputStream.close()会关掉整个socket连接,后续不能再操作
socket.shutdownOutput();
// 客户端读服务端返回的数据
byte[] buf = new byte[1024];
int len=0;
StringBuffer sb = new StringBuffer();
while((len=inputStream.read(buf))!=-1){
sb.append(new String(buf,0,len));
}
System.out.println("在时间点为" + System.nanoTime() + "时客户端读到服务端返回的数据【"+sb+"】");
socket.close();
System.out.println("客户端已关闭");
}
}
3、聊天室
服务端:
public class Server {
public static void main(String[] args) throws IOException {
System.out.println("=====服务端开启=====");
//创建一个服务器
System.out.println("等待客户端连接。。。");
PrintWriter pwtoclien = null;
Scanner keybordscanner = null;
Scanner inScanner = null;
ServerSocket ss = null;
try {
ss = new ServerSocket(8888);
//创建一个接收连接客户端的对象
Socket socket = ss.accept();
System.out.println(socket.getInetAddress() + "已成功连接到此台服务器上。");
//字符输出流
pwtoclien = new PrintWriter(socket.getOutputStream());
pwtoclien.println("已成功连接到远程服务器!" + "\t" + "请您先发言。");
pwtoclien.flush();
keybordscanner = new Scanner(System.in);
inScanner = new Scanner(socket.getInputStream());
//阻塞等待客户端发送消息过来
while (inScanner.hasNextLine()) {
String indata = inScanner.nextLine();
System.err.println("客户端:" + indata);
System.out.print("我(服务端):");
String keyborddata = keybordscanner.nextLine();
System.out.println("我(服务端):" + keyborddata);
pwtoclien.println(keyborddata);
pwtoclien.flush();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
pwtoclien.close();
keybordscanner.close();
inScanner.close();
try {
ss.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
客户端:
public class Client {
public static void main(String[] args) throws IOException {
System.out.println("正在向服务器请求连接。。。");
Socket socket = null;
Scanner keybordscanner = null;
Scanner inScanner = null;
PrintWriter pwtoserver = null;
try {
socket = new Socket("localhost", 8888);
inScanner = new Scanner(socket.getInputStream());
System.out.println(inScanner.nextLine());
pwtoserver = new PrintWriter(socket.getOutputStream());
System.out.print("我(客户端):");
//先读取键盘录入方可向服务端发送消息
keybordscanner = new Scanner(System.in);
while (keybordscanner.hasNextLine()) {
String keyborddata = keybordscanner.nextLine();
//展示到己方的控制台
System.out.println("我(客户端):" + keyborddata);
//写到服务端的的控制台
pwtoserver.println(keyborddata);
pwtoserver.flush();
//阻塞等待接收服务端的消息
String indata = inScanner.nextLine();
System.err.println("服务端:" + indata);
System.out.print("我(客户端):");
}
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
keybordscanner.close();
pwtoserver.close();
inScanner.close();
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}