计算机网络20世纪60年代出现,进入21世纪后,计算机网络已经成为信息社会的基础设施,深入到人类社会的方方面面,与人们的工作、学习和生活息息相关。计算机网络通过传输介质、通信设施和网络通信协议,把分散在不同地点的计算机设备互连起来的,实现资源共享和数据传输的系统。
网络编程就是编写程序使互联网的两个(或多个)设备(如计算机)之间进行数据传输。Java语言对网络编程提供了良好的支持。通过其提供的接口我们可以很方便地进行网络编程。
计算机网络体系结构是计算机网络层次和协议的集合,网络体系结构对计算机网络实现的功能,以及网络协议、层次、接口和服务进行了描述,但并不涉及具体的实现。接口是同一节点内相邻层之间交换信息的连接处,也叫服务访问点(SAP)。
IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。IP地址就像是我们的家庭住址一样,如果你要写信给一个人,你就要知道他(她)的地址,这样邮递员才能把信送到。
IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。
IP地址采用的IPv4格式,目前正在向IPv6过渡。IPv4采用32位地址长度,只有大约43亿个地址,估计在2005~2010年间将被分配完毕,IPv4定义的有限地址空间将被耗尽,而地址空间的不足必将妨碍互联网的进一步发展。而IPv6采用128位地址长度,几乎可以不受限制地提供地址。按保守方法估算IPv6实际可分配的地址,整个地球的每平方米面积上仍可分配1000多个地址。在IPv6的设计过程中除解决了地址短缺问题以外,还考虑了在IPv4中解决不好的其它一些问题,主要有端到端IP连接、服务质量(QoS)、安全性、多播、移动性、即插即用等。
IP地址是由网络号(net ID)与 主机号(host ID)两部分组成的。根据不同的取值范围,IP地址可以分为五类;
比如:192.168.1.2 掩码255.255.255.0 。网络位192.168.1 主机位是2
子网掩码(subnet mask)又叫网络掩码、地址掩码、子网络遮罩,它用来指明一个IP地址的哪些位标识的是主机所在的子网,以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合IP地址一起使用。
子网掩码将A、B、C三类地址划分为若干子网,从而显著提高了IP地址的分配效率,有效解决了IP地址资源紧张的局面。另一方面,在企业内网中为了更好地管理网络,网管人员也利用子网掩码的作用,人为地将一个较大的企业内部网络划分为更多个小规模的子网,再利用三层交换机的路由功能实现子网互联,从而有效解决了网络广播风暴和网络病毒等诸多网络管理方面的问题。
子网掩码机制提供了子网划分的方法。其作用是:减少网络上的通信量;节省IP地址;便于管理;解决物理网络本身的某些问题。使用子网掩码划分子网后,子网内可以通信,跨子网不能通信,子网间通信应该使用路由器,并正确配置静态路由信息。划分子网,就应遵循子网划分结构的规则。就是用连续的1在IP地址中增加表示网络地址,同时减少表示主机地址的位数。
比如利用子网数来计算:将B类IP:130.39.37.100划分成27个子网。
"端口"是英文port的意译,可以认为是设备与外界通讯交流的出口。用两个字节表示的整数,它的取值范围是0 - 65535。如果说IP地址可以唯一的标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序,也就是应用程序的标识。常见的端口号有:tomcat(8080)、mysql(3306)等。
按照端口号的大小分类,可分为如下几类:
由于IP地址具有不方便记忆并且不能显示地址组织的名称和性质等缺点,人们设计出了域名,并通过网域名称系统(DNS,Domain Name System)来将域名和IP地址相互映射,使人更方便地访问互联网,而不用去记住能够被机器直接读取的IP地址数串。
从语法上讲,每一个域名都是由标号序列组成,各标号之间用点隔开。如图所示的域名,由3个标号组成,其中标号cn是顶级域名,标号com是二级域名,标号example是三级域名,www是主机名。
在国家顶级域名下注册的二级域名由各国自行确定。我国把二级域名划分为类别域名和行政域名2大类。
类别域名:如ac(科研机构),com(工、商、金融等企业),edu(中国教育机构),gov(中国政府机构),mil(中国国防机构),net(提供互联网络服务的机构),org(非营利性的组织)等
行政区域名:共34个,适用于我国的各省、自治区、直辖市,如bj(北京市),js(江苏省),sn(陕西省),sx(山西省)等。
因特网的域名系统可以用域名树来表示其结构,它实际上是一棵倒过来的树,在最上面的是根,根下一级结点就是顶级域名,顶级域名可往下划分二级域名,再往下划分就是三级域名、四级域名。
域名到IP地址的解析是由分布在因特网上的许多域名服务器共同完成的。当某一个应用进程需要把主机名解析为IP地址时,该应用进程就调用解析程序,并将待解析的域名放在DNS请求报文中,以DUP用户数据报的方式发送给本地域名服务器,本地域名服务器再找到域名后,将对应的IP地址放在回答报文中返回,应用进程获得目的主机的IP地址后即可进行通信。
计算机网络中实现通信必须有一些约定,即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定标准。
最常用的复合方式是层次方式,网络通信的不同方面被分解为多个层,层与层之间用接口连接。通信的双方具有相同的层次,层次实现的功能由协议数据单元(PDU)来描述。不同系统中的同一层构成对等层,对等层之间通过对等层协议进行通信,理解批次定义好的规则和约定。每一层表示为物理硬件(即线缆和电流)与所传输信息之间的不同抽象层次。在理论上,每一层只与紧挨其上和其下的层对话。将网络分层,这样就可以修改甚至替换某一层的软件,只要层与层之间的接口保持不变,就不会影响到其他层。
开放式系统互联通信参考模型(英语:Open System Interconnection Reference Model,缩写为 OSI),简称为OSI模型(OSI model),一种概念模型,由国际标准化组织提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。定义于ISO/IEC 7498-1。
OSI模型把网络通信的工作分为7层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
(1)物理层
物理层处于OSI的最底层,是整个开放系统的基础。物理层涉及通信信道上传输的原始比特流(bits),它的功能主要是为数据端设备提供传送数据的通路以及传输数据。
(2)数据链路层
数据链路层的主要任务是实现计算机网络中相邻节点之间的可靠传输,把原始的、有差错的物理传输线加上数据链路协议以后,构成逻辑上可靠的数据链路。需要完成的功能有链路管理、成帧、差错控制以及流量控制等。其中成帧是对物理层的原始比特流进行界定,数据链路层也能够对帧的丢失进行处理。
(3)网络层
网络层涉及源主机节点到目的主机节点之间可靠的网络传输,它需要完成的功能主要包括路由选择、网络寻址、流量控制、拥塞控制、网络互连等。
(4)传输层
传输层起着承上启下的作用,涉及源端节点到目的端节点之间可靠的信息传输。传输层需要解决跨越网络连接的建立和释放,对底层不可靠的网络,建立连接时需要三次握手,释放连接时需要四次挥手。
(5)会话层
会话层的主要功能是负责应用程序之间建立、维持和中断会话,同时也提供对设备和结点之间的会话控制,协调系统和服务之间的交流,并通过提供单工、半双工和全双工3种不同的通信方式,使系统和服务之间有序地进行通信。
(6)表示层
表示层关心所传输数据信息的格式定义,其主要功能是把应用层提供的信息变换为能够共同理解的形式,提供字符代码、数据格式、控制信息格式、加密等的统一表示。
(7)应用层
应用层为OSI的最高层,是直接为应用进程提供服务的。其作用是在实现多个系统应用进程相互通信的同时,完成一系列业务处理所需的服务。
由于OSI参考模型过于庞大、复杂招致了许多批评。与此相对,美国国防部提出了TCP/IP协议栈参考模型,简化了OSI参考模型,因为TCP/IP协议栈的简单,获得了广泛的应用,并成为后续因特网使用的参考模型。
TCP/IP协议是一个开放的网络协议簇,它的名字主要取自最重要的网络层IP协议和传输层TCP协议。TCP/IP协议定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。TCP/IP参考模型采用4层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求,这4个层次分别是:网络接口层、网络层(IP层)、传输层(TCP层)、应用层。
(1)网络接口层
这是TCP/IP软件的最低层,负责接收IP数据报并通过网络发送之,或者从网络上接收物理帧,抽出IP数据报,交给IP层。
(2)网络层
网络层是整个TCP/IP协议栈的核心。它的功能是把分组发往目标网络或主机。同时,为了尽快地发送分组,可能需要沿不同的路径同时进行分组传递。因此,分组到达的顺序和发送的顺序可能不同,这就需要上层必须对分组进行排序。网络层除了需要完成路由的功能外,也可以完成将不同类型的网络(异构网)互连的任务。除此之外,互联网层还需要完成拥塞控制的功能。
(3)传输层
负责在应用进程之间建立端到端的连接和可靠通信,它只存在与端节点中。涉及两个协议,TCP和UDP。其中,TCP协议提供面向连接的服务,提供按字节流的有序、可靠传输,可以实现连接管理、差错控制、流量控制、拥塞控制等。UDP协议提供无连接的服务,用于不需要或无法实现面向连接的网络应用中。
(4)应用层
为各种网络应用提供服务。
TCP/IP协议报文段包括协议首部和数据两部分,协议首部的固定部分是20个字节,首部的固定部分后面是选项部分。
下面是报文段首部各个字段的含义:
源端口号以及目的端口号:各占2个字节,端口是传输层和应用层的服务接口,用于寻找发送端和接收端的进程,一般来讲,通过端口号和IP地址,可以唯一确定一个TCP/IP连接,在网络编程中,通常被称为一个socket接口。
序号:Seq序号,占4个字节、32位。用来标识从TCP/IP发送端向TCP/IP接收端发送的数据字节流。发起方发送数据时对此进行标记。
确认序号:Ack序号,占4个字节、32位。包含发送确认的一端所期望收到的下一个序号。只有ACK标记位为1时,确认序号字段才有效,因此,确认序号应该是上次已经成功收到数据字节序号加1,即Ack=Seq + 1。
数据偏移:占4个字节,用于指出TCP/IP首部长度,若不存在选项,则这个值为20字节,数据偏移的最大值为60字节。
保留字段占6位,暂时可忽略,值全为0。
标志位,6个
(1)URG(紧急):为1时表明紧急指针字段有效
(2)ACK(确认):为1时表明确认号字段有效
(3)PSH(推送):为1时接收方应尽快将这个报文段交给应用层
(4)RST(复位):为1时表明TCP连接出现故障必须重建连接
(5)SYN(同步):在连接建立时用来同步序号
(6)FIN(终止):为1时表明发送端数据发送完毕要求释放连接
接收窗口:占2个字节,用于流量控制和拥塞控制,表示当前接收缓冲区的大小。在计算机网络中,通常是用接收方的接收能力的大小来控制发送方的数据发送量。TCP/IP连接的一端根据缓冲区大小确定自己的接收窗口值,告诉对方,使对方可以确定发送数据的字节数。
校验和:占2个字节,范围包括首部和数据两部分。
选项是可选的,默认情况是不选。
TCP/IP是面向连接的协议,因此每个TCP/IP连接都有3个阶段:连接建立、数据传送和连接释放。连接建立经历三个步骤,通常称为“三次握手”。三次握手的目的是同步连接双方的序列号和确认号并交换 TCP/IP窗口大小信息。
客户端发送连接请求。发送连接请求报文段内容:SYN=1,seq=x;SYN=1意思是一个TCP的SYN(同步序列)标志位置为1的包,指明客户端打算连接的服务器的端口;seq=x表示客户端初始序号x,保存在包头的序列号(Sequence Number)字段里,进入SYN—SEND状态,等待服务器的确认。
服务器收到客户端连接请求报文,如果同意建立连接,向客户机发回确认报文段(ACK)应答,并为该TCP连接分配TCP缓存和变量。服务器发回确认报文段内容:SYN=1,ACK=1,seq=y,ack=x+1;SYN标志位和ACK标志位均为1,同时将确认序号(Acknowledgement Number)设置为客户的初始序列号 ( Initial Sequence Number) ISN 加1,即x+1;seq=y为服务端初始序号y。服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN—RECV状态。
客户机收到服务器的确认报文段后,向服务器给出确认报文段(ACK),并且也要给该连接分配缓存和变量。此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。客户端发回确认报文段内容:ACK=1,seq=x+1,ack=y+1;ACK=1为确认报文段;seq=x+1为客户端序号加1;ack=y+1,为服务器发来的ACK的初始序号字段+1。
我们先来对比下TCP/IP和UDP的区别:我们都知道TCP/IP是可靠通信协议, 而 UDP 是不可靠通信协议,所以UDP会导致接收方接收到的数据可能存在部分丢失。
TCP/IP是一个双向通信协议, 通信双方都有能力发送信息, 并接收响应。
(1)防止旧的重复连接引起连接混乱
如果 TCP/IP 握手的次数只有两次,接收方只能选择接受请求或者拒绝接受请求,在网络状况比较复杂或者网络状况比较差的情况下,发送方可能会连续发送多次建立连接的请求,某个网络结点长时间的滞留了,在某个时间节点到达服务端,此时就有可能任务是服务端发起的新请求,向客户端发生确认报文,建立连接,导致错误的连接。所以如果 TCP/IP 是三次握手的话,那么客户端在接收到服务器端 SEQ+1 的消息之后,就可以判断当前的连接是否为历史连接,如果判断为历史连接的话就会发送终止报文(RST)给服务器端终止连接,否则就会发送指令给服务器端来建立连接。
(2)同步初始化序列化
通过上面的概念我们知道 TCP/IP的一个重要特征就是可靠性,而 TCP/IP为了保证在不稳定的网络环境中构建一个稳定的数据连接,它就需要一个“序列号”字段来保证自己的稳定性,而这个序列号的作用就是防止数据包重复发送,以及有效的解决数据包接收时顺序颠倒的问题。客户端和服务端通过三次握手建立连接后,得到一个可靠的初始化序列号。如果只是两次握手, 最多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。
当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。所以没有必要将SYN和ACK单独分开,避免资源浪费,三次握手是建立双方连接的最低值。
当客户端和服务器通过三次握手建立了TCP连接以后,就可以进行数据传输,当数据传送完毕,就要进行关闭操作,需要经历四个步骤。
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
TCP客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入FIN-WAIT-1状态,表示客户端没有数据要发送给服务端了。
服务端收到这个FIN,它发回一个ACK给客户端,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号;服务端进入CLOSE-WAIT状态,服务端告诉客户端,我“同意”你的关闭请求。
服务端向客户端发送FIN报文段,请求关闭连接,服务端进入LAST-ACK状态。
客户端收到FIN后,客户端进入TIME-WAIT状态,接着发回一个ACK报文给服务端确认,并将确认序号设置为收到序号加1,服务端进入CLOSED状态,客户端等待2MSL后依然没有收到回复,则证明服务端已正常关闭,客户端也可以关闭连接了,完成四次挥手。
关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
可以的,我们知道第三次挥手是为了确认服务端没有要发送的数据,如果第一次挥手后服务端没有数据要发送,是有可能将第二次和第三次挥手合并的,这样就是三次挥手了。如果服务端有数据,难道就不能进行三次挥手了吗?也是不一定的,TCP中有个延迟确认的特性,客户端收到数据后,可以不用马上进行ACK确认,可能将多个数据包合并一个确认包,再将确认包放到第四次挥手里,然后把第二次和第三次挥手合并,这样也就是三次挥手了。
UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议。传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
虽然UDP是一个不可靠的协议,但它是分发信息的一个理想协议。例如,在屏幕上报告股票市场、显示航空信息等等。UDP也用在路由信息协议RIP(Routing Information Protocol)中修改路由表。在这些应用场合下,如果有一个消息丢失,在几秒之后另一个新的消息就会替换它。UDP广泛用在多媒体应用中。
超文本传输协议(Hyper Text Transfer Protocol,HTTP)是一个简单的请求-响应协议,HTTP是应用层协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII形式给出;而 消息内容则具有一个类似MIME的格式。这个简单模型是早期Web成功的有功之臣,因为它使开发和部署非常地直截了当。
典型的HTTP事务处理有如下的过程:
HTTP报文由从客户机到服务器的请求和从服务器到客户机的响应构成。请求报文格式如下:
HTTP响应报文由状态行、首部行和实体主体组成。
除了上面案例中的POST请求方法,还有很多其它请求方法,如图所示:
在HTTP响应报文的例子中,我们可以看到状态码是200,表示响应成功。
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和HTTPS的区别:
超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息。HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此HTTP协议不适合传输一些敏感信息,比如信用开号、密码等。
为了解决HTTP协议的这一缺陷,需要使用另一种协议:安全套接字层超文本传输协议HTTPS。为了数据传输的安全,HTTPS在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。
HTTPS和HTTP的区别主要为以下四点:
Java的网络编程主要涉及到的内容是套接字(Socket)编程。Socket是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。Socket本身不算是协议,它只是提供了一个针对TCP或者UDP编程的接口。Socket是对端口通信开发的工具,它要更底层一些。
Socket编程主要涉及到客户端和服务端两个方面,首先是在服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到65536,但是0到1024是为特权服务保留的端口号,我们可以选择任意一个当前没有被其他进程使用的端口。
客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。
java.net.IntAddress
类封装计算机的IP地址,没有端口。java.net.Socket
类代表一个套接字,而且 java.net.ServerSocket
类为服务器程序提供了一种机制来监听客户端并和它们建立连接。
InetAddress类的两个子类:Inet4Address、Inet6Address,一个用于表示IPV4协议,另一个表示IPV6协议
InetAddress类不能直接创建,通过静态方法创建对象。列举常用方法:
getByName(String host)
:通过主机名或域名得到InetAddress对象getLocalHost()
:获取本地主机的地址。getLoopbackAddress()
:获取IPV4或者IPV6回环对象。getAllByName(String host)
:获取指定主机名的所有IP地址的数组。示例代码如下:
public class NetworkProgramming {
public static void main(String[] args) {
InetAddress inetAddress = InetAddress.getByName("localhost");
System.out.println(inetAddress);
inetAddress = InetAddress.getLocalHost();
System.out.println(inetAddress);
inetAddress = InetAddress.getLoopbackAddress();
System.out.println(inetAddress);
InetAddress[]inetAddress2 = InetAddress.getAllByName("www.baidu.com");
System.out.println(Arrays.toString(inetAddress2));
/** Output:
* localhost/127.0.0.1
* DESKTOP-4K4SA9I/192.168.36.118
* localhost/127.0.0.1
* [www.baidu.com/180.101.49.12, www.baidu.com/180.101.49.11]
*/
}
}
创建后,还有许多方法可以进行获取和判断操作,列举一部分代码,示例代码如下:
public class NetworkProgramming {
public static void main(String[] args) {
InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
System.out.println("规范的主机名:"+inetAddress.getCanonicalHostName());
System.out.println("主机名:"+inetAddress.getHostName());
System.out.println("是否为通配符地址:"+inetAddress.isAnyLocalAddress());
System.out.println("是否为链接本地地址:"+inetAddress.isLinkLocalAddress());
System.out.println("是否全局范围的多播地址:"+inetAddress.isMCGlobal());
System.out.println("是否链路本地作用域的多播地址:"+inetAddress.isMCLinkLocal());
System.out.println("是否为一个IP组播地址:"+inetAddress.isMulticastAddress());
System.out.println("地址是否可达:"+inetAddress.isReachable(1000));
System.out.println("是否为网站本地地址:"+inetAddress.isSiteLocalAddress());
/** Output:
* 规范的主机名:180.101.49.12
* 主机名:www.baidu.com
* 是否为通配符地址:false
* 是否为链接本地地址:false
* 是否全局范围的多播地址:false
* 是否链路本地作用域的多播地址:false
* 是否为一个IP组播地址:false
* 地址是否可达:true
* 是否为网站本地地址:false
*/
}
}
除此之外还有IntetSocketAddress类,与InetAddress 类不同的是增加了端口号(port),创建方法如下:
createUnresolved(String host, int port)
:未解析的套接字地址 。new InetSocketAddress(int port)
:创建一个套接字地址,其中IP地址是通配符地址和指定值的端口号。new InetSocketAddress(String hostname, int port)
:使用主机名和端口号创建套接字地址。new InetSocketAddress(InetAddress addr, int port)
:使用InetAddress对象和端口号创建套接字地址。示例代码如下:
public class NetworkProgramming {
public static void main(String[] args) {
InetSocketAddress inetSocketAddress = InetSocketAddress.createUnresolved("localhost",8088);
System.out.println(inetSocketAddress);
inetSocketAddress = new InetSocketAddress(8088);
System.out.println(inetSocketAddress);
inetSocketAddress = new InetSocketAddress("192.168.16.118",8088);
System.out.println(inetSocketAddress);
InetAddress inetAddress = InetAddress.getByName("192.168.16.118");
inetSocketAddress = new InetSocketAddress(inetAddress,8088);
System.out.println(inetSocketAddress);
/** Output:
* localhost:8088
* 0.0.0.0/0.0.0.0:8088
* /192.168.16.118:8088
* /192.168.16.118:8088
*/
}
}
当然IntetSocketAddress类也有属于自己的方法,示例代码如下:
public class NetworkProgramming {
public static void main(String[] args) {
InetSocketAddress inetSocketAddress = new InetSocketAddress("www.baidu.com",8088);
System.out.println("解析地址:"+inetSocketAddress.getAddress());
System.out.println("主机名:"+inetSocketAddress.getHostName());
System.out.println("端口号:"+inetSocketAddress.getPort());
System.out.println("主机名或地址:"+inetSocketAddress.getHostString());
System.out.println("是否解析:"+inetSocketAddress.isUnresolved());
/** Output:
* 解析地址:www.baidu.com/180.101.49.12
* 主机名:www.baidu.com
* 端口号:8088
* 主机名或地址:www.baidu.com
* 是否解析:false
*/
}
}
Socket是一组编程接口(API), 是对TCP/IP协议的封装和应用。java.net.Socket 类方法代表客户端和服务器都用来互相通信的套接字。客户端通过实例化而拥有一个 Socket 对象,然后服务器从 accept()
方法的返回值获得一个 Socket 对象。
通过创建Socket 类可以监听指定端口,列举常用的构造函数,示例代码如下:
new Socket()
:创建一个未连接的套接字。使用 connect()
方法来连接这个套接字到服务器。new Socket(String host, int port)
:创建一个流套接字并将其连接到指定的主机和端口号。new Socket(InetAddress address, int port)
:创建一个流套接字并将其连接到指定的端口和指定的IP地址。new Socket(String host, int port, InetAddress localAddr,int localPort)
:创建套接字并将其连接到上指定的远程主机指定的远端端口。Socket也将bind()
绑定到本地提供的地址和端口。public class Client {
public static void main(String[] args) {
Socket socket = new Socket();
socket = new Socket("localhost",3306);
socket = new Socket(InetAddress.getLocalHost(),3306);
socket = new Socket(InetAddress.getByName("www.baidu.com"),443,InetAddress.getLocalHost(),9966);
}
}
Socket类中还有一些比较常用的方法,示例代码如下:
public class Client {
public static void main(String[] args) {
Socket socket = new Socket("localhost",3306);
System.out.println("nio通道对象:"+socket.getChannel());
System.out.println("是否启用:"+socket.getKeepAlive());
System.out.println("本地端口:"+socket.getLocalPort());
System.out.println("远程端口:"+socket.getPort());
System.out.println("连接地址:"+socket.getInetAddress());
System.out.println("本地地址:"+socket.getLocalAddress());
System.out.println("远程地址:"+socket.getRemoteSocketAddress());
System.out.println("输入流:"+socket.getInputStream());
System.out.println("输出流:"+socket.getOutputStream());
System.out.println("是否连接成功:"+socket.isConnected());
System.out.println("是否绑定成功:"+socket.isBound());
System.out.println("是否关闭:"+socket.isClosed());
System.out.println("读取是否关闭:"+socket.isInputShutdown());
System.out.println("写入是否关闭:"+socket.isOutputShutdown());
SocketAddress socketAddress = Proxy.NO_PROXY.address();
//连接
socket.connect(socketAddress);
//绑定
socket.bind(socketAddress);
//关闭
socket.close();
//设置socket是否激活
socket.setKeepAlive(true);
//设置网络缓冲区大小
socket.setSendBufferSize(1024);
/** Output:
* nio通道对象:null
* 是否启用:false
* 本地端口:58154
* 远程端口:3306
* 连接地址:localhost/127.0.0.1
* 本地地址:/127.0.0.1
* 远程地址:localhost/127.0.0.1:3306
* 输入流:java.net.SocketInputStream@29453f44
* 输出流:java.net.SocketOutputStream@5cad8086
* 是否连接成功:true
* 是否绑定成功:true
* 是否关闭:false
* 读取是否关闭:false
* 写入是否关闭:false
*/
}
}
仅仅只有Socket类是不足以编写服务器的,java.net.ServerSocket
类被服务器应用程序使用来获得一个端口和监听客户端请求。ServerSocket使用accept()
方法监听这个端口的入站连接。accept()
会一直阻塞,直到一个客户端尝试建立连接,此时accept()
将会返回一个连接客户端和服务器的Socket对象。
列举常用的构造函数,示例代码如下:
new ServerSocket()
:创建未绑定的服务器套接字。new ServerSocket(int port)
:创建与指定端口绑定的服务器套接字。new ServerSocket(int port, int backlog)
:创建服务器套接字并将其绑定到指定的本地端口,设置请求的传入连接队列的最大长度。new ServerSocket(int port, int backlog, InetAddress bindAddr)
:创建一个具有指定端口的服务器,设置请求的传入连接队列的最大长度以及绑定的本地IP地址。public class Client {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
serverSocket = new ServerSocket(80);
serverSocket = new ServerSocket(80,5);
serverSocket = new ServerSocket(80,5,InetAddress.getByName("www.baidu.com"));
}
}
ServerSocket类中还有一些比较常用的方法,示例代码如下:
public class Client {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(80);
System.out.println("nio通道对象:"+serverSocket.getChannel());
System.out.println("本地端口:"+serverSocket.getLocalPort());
System.out.println("连接地址:"+serverSocket.getInetAddress());
System.out.println("是否绑定成功:"+serverSocket.isBound());
System.out.println("是否关闭:"+serverSocket.isClosed());
//连接
serverSocket.accept();
//绑定
serverSocket.bind(Proxy.NO_PROXY.address());
//关闭
serverSocket.close();
/** Output:
* nio通道对象:null
* 本地端口:80
* 连接地址:0.0.0.0/0.0.0.0
* 是否绑定成功:true
* 是否关闭:false
*/
}
}
客户端,示例代码如下:
public class Client {
public static void main(String[] args) throws IOException {
try(Socket socket = new Socket("192.168.36.118", 996);
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();){
System.out.println("客户端发送消息:");
outputStream.write("你好".getBytes());
//关键代码:告诉服务端结束输出,否则服务端收到消息会一直阻塞
socket.shutdownOutput();
//outputStream.flush();
byte[] bytes = new byte[1024];
int len = 0;
while ((len = inputStream.read(bytes)) != -1) {
System.out.println("客户端接收消息:" + new String(bytes, 0, len));
}
}catch (Exception e){
}
/** Output:
* 客户端发送消息:
* 客户端接收消息:你好,很高兴认识你
*/
}
}
服务端,示例代码如下:
public class NetworkProgramming {
public static void main(String[] args) throws IOException {
try(ServerSocket serverSocket = new ServerSocket(996);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();) {
byte[] bytes = new byte[1024];
int len = 0;
while ((len = inputStream.read(bytes)) != -1) {
System.out.println("服务端接收消息:" + new String(bytes, 0, len));
}
System.out.println("服务端发送消息");
outputStream.write("你好,很高兴认识你".getBytes());
}catch (Exception e){
e.printStackTrace();
}
/** Output:
* 服务端接收消息:你好
* 服务端发送消息
*/
}
}
基本原理和发送消息一直,只不过把内容的主题换成了文件。
客户端,示例代码如下:
public class Client {
public static void main(String[] args) throws IOException {
byte[] bytes = new byte[1024];
int len = 0;
try (Socket socket = new Socket("192.168.36.118", 996);
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();
FileInputStream fileInputStream = new FileInputStream("C:\\mnt\\client.gif");
FileOutputStream fileOutputStream = new FileOutputStream("client\\server.jpg");) {
System.out.println("客户端发送图片");
while ((len = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
}
//关键代码:告诉服务端结束输出,否则服务端会一直阻塞
socket.shutdownOutput();
//outputStream.flush();
System.out.println("收到服务端图片并保存");
while ((len = inputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, len);
}
} catch (Exception e) {
}
/** Output:
* 客户端发送图片
* 收到服务端图片并保存
*/
}
}
服务端,示例代码如下:
public class NetworkProgramming {
public static void main(String[] args) throws IOException {
byte[] bytes = new byte[1024];
int len = 0;
try(ServerSocket serverSocket = new ServerSocket(996);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
FileInputStream fileInputStream = new FileInputStream("C:\\mnt\\server.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("server\\client.gif");) {
System.out.println("收到客户端图片并保存");
while ((len = inputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes,0,len);
}
System.out.println("服务端发送图片");
while ((len = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes,0,len);
}
}catch (Exception e){
e.printStackTrace();
}
/** Output:
* 收到客户端图片并保存
* 服务端发送图片
*/
}
}
先来展示错误的长连接方式。
客户端,示例代码如下:
public class Client {
public static void main(String[] args) throws IOException {
try(Socket socket = new Socket("192.168.131.1", 996);
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();){
while (true) {
System.out.println("客户端发送消息:");
Scanner scanner = new Scanner(System.in);
String b = scanner.next();
outputStream.write(b.getBytes());
//关键代码:告诉服务端结束输出,否则服务端收到消息会一直阻塞
socket.shutdownOutput();
//outputStream.flush();
byte[] bytes = new byte[1024];
int len = 0;
while ((len = inputStream.read(bytes)) != -1) {
System.out.println("客户端接收消息:" + new String(bytes, 0, len));
}
}
}catch (Exception e){
e.printStackTrace();
}
/** Output:
* 客户端发送消息:
* 11
* 客户端接收消息:222
* 客户端接收消息:222
* 客户端接收消息:qwe
* 客户端接收消息:qwe
* 客户端接收消息:qwe
* 客户端接收消息:qwe
* 客户端接收消息:qwe
*/
}
}
服务端,示例代码如下:
public class NetworkProgramming {
public static void main(String[] args) throws IOException {
try(ServerSocket serverSocket = new ServerSocket(996);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();) {
while (true) {
byte[] bytes = new byte[1024];
int len = 0;
while ((len = inputStream.read(bytes)) != -1) {
System.out.println("服务端接收消息:" + new String(bytes, 0, len));
}
System.out.println("服务端发送消息");
Scanner scanner = new Scanner(System.in);
String b = scanner.next();
outputStream.write(b.getBytes());
}
}catch (Exception e){
e.printStackTrace();
}
/** Output:
* 服务端接收消息:11
* 服务端发送消息
* 222
* 服务端发送消息
* 222
* 服务端发送消息
*/
}
}
通过输出可以看到,当客户端第一次发送消息后,服务端接收完消息,再由服务端发送时,由于没有关闭输出操作,导致客户端一直处于监听状态,但是如果服务端关闭输出操作;客户端再发送消息时,此时双方发送都已关闭,这会导致异常错误。我们想要实现双方都可以发送或接受消息,示例代码如下:
客户端,示例代码如下:
public class Client {
private static Executor executor = Executors.newCachedThreadPool();
public static void main(String[] args) throws IOException {
executor.execute(new Runnable() {
@Override
public void run() {
try (Socket socket = new Socket("192.168.131.1", 996);
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();) {
while (true) {
System.out.println("客户端发送消息:");
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
outputStream.write(s.getBytes());
//强制刷新输出
outputStream.flush();
byte[] bytes = new byte[1024];
int len = inputStream.read(bytes);
s = new String(bytes, 0, len);
System.out.println("客户端接收消息:" + s);
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
});
/** Output:
* 客户端发送消息:
* 你好,很高兴认识你!
* 客户端接收消息:我也是,怎么称呼你?
*/
}
}
服务端,示例代码如下:
public class NetworkProgramming {
private static Executor executor = Executors.newCachedThreadPool();
public static void main(String[] args) throws IOException {
executor.execute(new Runnable() {
@Override
public void run() {
try (ServerSocket serverSocket = new ServerSocket(996);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();) {
while (true) {
byte[] bytes = new byte[1024];
int len = 0;
while ((len = inputStream.read(bytes)) != -1) {
String s = new String(bytes, 0, len);
System.out.println("服务端接收消息:" + s);
System.out.println("服务端发送消息:");
Scanner scanner = new Scanner(System.in);
s = scanner.nextLine();
outputStream.write(s.getBytes());
//强制刷新输出
outputStream.flush();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
/** Output:
* 服务端接收消息:你好,很高兴认识你!
* 服务端发送消息:
* 我也是,怎么称呼你?
*/
}
}
这样就实现了一个简单的聊天工具,不过单方面通讯不能发送多条信息,算是一个小遗憾,有机会再研究,有想法的可以评论区讨论!!!
UDP编程主要有两个类:DatagramSocket类和DatagramPacket类,UDP数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的 IP 地址和端口号以及接收端的IP地址和端口号。UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接。这里不在介绍每个类的创建方式和方法了,有兴趣的可以自己去了解。
发送方,示例代码如下:
public class NetworkProgramming {
public static void main(String[] args) throws IOException {
try (DatagramSocket datagramSocket = new DatagramSocket();) {
String str = "第一次发送数据";
DatagramPacket datagramPacket = new DatagramPacket(str.getBytes(), str.getBytes().length, InetAddress.getByName("localhost"), 996);
datagramSocket.send(datagramPacket);
}
}
}
接收方,示例代码如下:
public class Client {
public static void main(String[] args) throws IOException {
try (DatagramSocket datagramSocket = new DatagramSocket(996);) {
byte[] bytes = new byte[1024];
//数据报包
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("localhost"), 996);
datagramSocket.receive(datagramPacket);
System.out.println("接收方接收数据:" + new String(bytes));
}
/** Output:
* 接收方接收数据:第一次发送数据
*/
}
}
发送方,示例代码如下:
public class NetworkProgramming {
public static void main(String[] args) throws IOException {
//new DatagramSocket(996);监听接收端口
try (DatagramSocket datagramSocket = new DatagramSocket(997);) {
while (true) {
System.out.print("发送方发送数据:");
String str = new Scanner(System.in).nextLine();
DatagramPacket datagramPacket = new DatagramPacket(str.getBytes(), str.getBytes().length, InetAddress.getByName("localhost"), 996);
datagramSocket.send(datagramPacket);
byte[] bytes = new byte[1024];
//数据报包
datagramPacket = new DatagramPacket(bytes, bytes.length);
datagramSocket.receive(datagramPacket);
System.out.println("发送方接收数据:" + new String(bytes));
}
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
/** Output:
* 发送方发送数据:你好,很高兴认识你!
* 发送方接收数据:我也是,要不出来见个面把。
* 发送方发送数据:嗯嗯
*/
}
}
接收方,示例代码如下:
public class Client {
public static void main(String[] args) throws IOException {
//new DatagramSocket(996);监听接收端口
try (DatagramSocket datagramSocket = new DatagramSocket(996);) {
while (true) {
byte[] bytes = new byte[1024];
//数据报包
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
datagramSocket.receive(datagramPacket);
System.out.println("接收方接收数据:" + new String(bytes));
System.out.print("接收方发送数据:");
String str = new Scanner(System.in).nextLine();
datagramPacket = new DatagramPacket(str.getBytes(), str.getBytes().length, InetAddress.getByName("localhost"), 997);
datagramSocket.send(datagramPacket);
}
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
/** Output:
* 接收方接收数据:你好,很高兴认识你!
* 接收方发送数据:我也是,要不出来见个面把。
* 接收方接收数据:嗯嗯
*/
}
}
经过TCP/IP和UDP代码的代码实战,很容易发现它们之间的区别:TCP/IP建立一个端口就可以实现客户端和服务端之间的交互,而UDP不同,发送方和接收发实际上是两个不同的端口,只不过担当同一个角色的两个不同的操作,TCP/IP就有点类似于同一个角色同一个操作。
URL(Uniform Resource Locator)中文名为统一资源定位符,有时也被俗称为网页地址。表示为互联网上的资源,如网页或者 FTP 地址。
在java.net
包中定义了URL类,该类用来处理有关URL的内容。
public class NetworkProgramming {
public static void main(String[] args) throws IOException {
URL url = new URL("https","baike.baidu.com",80,"/item/Java/85979?fr=aladdin#5");
URL url = new URL("https","baike.baidu.com","/item/Java/85979?fr=aladdin#5");
URL url = new URL(new URL("https://baike.baidu.com"),"/item/Java/85979?fr=aladdin#5");
URL url = new URL("https://baike.baidu.com/item/Java/85979?fr=aladdin#5");
}
}
一共有四种创建方式,以第一种创建方式为例:第一个参数表示协议、第二个参数表示主机、第三个参数表示端口、第四个参数表示文件地址。
URL类中包含了很多方法用于访问URL的各个部分:
public class NetworkProgramming {
public static void main(String[] args) throws IOException {
URL url = new URL("https://baike.baidu.com/item/Java/85979?fr=aladdin#5");
System.out.println("url内容:" + url.getContent());
System.out.println("url授权部分:" + url.getAuthority());
System.out.println("url默认端口:" + url.getDefaultPort());
System.out.println("url文件信息::" + url.getFile());
System.out.println("url主机:" + url.getHost());
System.out.println("url端口号:" + url.getPort());
System.out.println("url协议:" + url.getProtocol());
System.out.println("url参数:" + url.getQuery());
System.out.println("url锚链接:" + url.getRef());
System.out.println("url路径:" + url.getPath());
/** Output:
* url内容:sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@66d2e7d9
* url授权部分:baike.baidu.com
* url默认端口:443
* url文件信息::/item/Java/85979?fr=aladdin
* url主机:baike.baidu.com
* url端口号:-1
* url协议:https
* url参数:fr=aladdin
* url锚链接:5
* url路径:/item/Java/85979
*/
}
}
当你知道一个地址后,如果你需要使用它,就得先连接上:
public class NetworkProgramming {
public static void main(String[] args) throws IOException {
URL url = new URL("https://baike.baidu.com/item/Java/85979?fr=aladdin#5");
URLConnection urlConnection = url.openConnection();
}
}
连接成功后,可以获取页面的一些信息,下面列举了比较常用的方法:
public class NetworkProgramming {
public static void main(String[] args) throws IOException {
URL url = new URL("https://baike.baidu.com/item/Java/85979?fr=aladdin#5");
URLConnection urlConnection = url.openConnection();
//设置地址可用于读取
urlConnection.setDoInput(true);
//设置地址可用于输出
urlConnection.setDoOutput(true);
//设置连接超时时间
urlConnection.setConnectTimeout(1000);
//设置读取超时时间
urlConnection.setReadTimeout(1000);
System.out.println("获取读取状态:"+urlConnection.getDoInput());
System.out.println("获取输出状态:"+urlConnection.getDoOutput());
System.out.println("获取连接状态:"+urlConnection.getConnectTimeout());
System.out.println("获取读取超时时间:"+urlConnection.getReadTimeout());
System.out.println("获取报文头(header)内容长度:"+urlConnection.getContentLength());
System.out.println("获取报文头(header)类型:"+urlConnection.getContentType());
System.out.println("获取报文头(header)编码格式:"+urlConnection.getContentEncoding());
//输入流
urlConnection.getInputStream();
//输出流
urlConnection.getOutputStream();
/** Output:
* 获取读取状态:true
* 获取输出状态:true
* 获取连接状态:1000
* 获取读取超时时间:1000
* 获取报文头(header)内容长度:-1
* 获取报文头(header)类型:text/html; charset=UTF-8
* 获取报文头(header)编码格式:null
*/
}
}
下面就介绍通过URL获取某网站经典歌曲《两只老虎》为例,示例代码如下:
public class NetworkProgramming {
public static void main(String[] args) throws IOException {
URL url = new URL("http://music.163.com/song/media/outer/url?id=566443167.mp3");
URLConnection urlConnection = url.openConnection();
try (InputStream inputStream = urlConnection.getInputStream();
FileOutputStream fileOutputStream = new FileOutputStream("两只老虎.mp3");) {
byte[] bytes = new byte[1024];
int i = 0;
while ((i = inputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
网络编程从大的方面说就是对信息的发送到接收,中间传输为物理线路的作用。网络编程设计的内容也是非常的广,一起加油。