网络通信协议:通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换 网络编程: 指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。 java.net 包中 J2SE 的 API 包含有类和接口,它们提供低层次的通信细节。你可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。 java.net 包中提供了两种常见的网络协议的支持:
- TCP:TCP(英语:Transmission Control Protocol,传输控制协议) 是一种面向连接的、可靠的、基于字节流的传输层通信协议,TCP 层是位于 IP 层之上,应用层之下的中间层。TCP 保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称 TCP / IP。
- UDP:UDP(英语:User Datagram Protocol,用户数据报协议) ,位于 OSI 模型的传输层。一个无连接的协议。提供了应用程序之间要发送数据的数据报。由于UDP缺乏可靠性且属于无连接协议,所以应用程序通常必须容许一些丢失、错误或重复的数据包
本教程主要讲解以下两个主题。
- URL 处理:这部分会在另外的篇幅里讲,点击这里更详细地了解在 Java 语言中的 URL 处理
- Socket 编程:在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据
- 所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制
- Socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open -> 读写write/read -> 关闭close”模式来操作
- 我的理解就是Socket就是该模式的一个实现:即socket是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
- Socket()函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的
网络编程三要素
利用 协议+IP地址+端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互
协议
IP地址
指互联网协议地址(Internet Protocol Address),IP地址用来给一个网络中的计算机设备做唯一的编号
IP地址分类
1字节=8位比特 1B=8bitIPv432位(4段8位2进制)的二进制数,4个字节分段表示,例如192.168.65.100其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个IPv6128位地址长度,16位字节分段表示,分成8段十六进制数例如:ABCD:EF01:2345:6789:ABCD:EF01:2345:6789号称可以为全世界的每一粒沙子编上一个网址
查看本机IP地址,在控制台输入 ipconfig
检查网络是否连通,在控制台输入 ping www.baidu.com
端口号
用两个字节表示的整数,它的取值范围是0~65535。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败
常见端口号
80 http网络端口
3306 mysql
1521 orcel
8080 Tomat服务器
TCP与UDP
TCP 传输控制协议 (Transmission Control Protocol)
TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输
在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求
每次连接的创建都需要经过“三次握手”,以保证连接的可靠
TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等
UDP 用户数据报协议(User Datagram Protocol)
- UDP是是无连接通信协议:即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据
- 使用UDP协议消耗资源小,通信效率高:所以通常都会用于音频、视频和普通数据的传输例如视频会议都使
UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响
使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。UDP的交换过程如下图所示
特点:数据被限制在64kb以内,超出这个范围就不能发送了,数据报(Datagram):网络传输的基本单位
软件结构
C/S结构
全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件,两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序
B/S结构
全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等
TCP/IP协议
TCP/IP协议概念
TCP/IP是Internet最基本、最广泛的协议
传输控制协议/因特网互联协议(Transmission Control Protocol/Internet Protocol)
- 提供点对点的链接机制,将数据应该如何封装、定址、传输、路由以及在目的地如何接收,都加以标准化。
- 它将软件通信过程抽象化为四个抽象层,采取协议堆栈的方式,分别实现出不同通信协议。协议族下的各种协议,依其功能不同,被分别归属到这四个层次结构之中,常被视为是简化的七层OSI模型。
- 它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求
- TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,数据在传输前要建立连接,传输完毕后还要断开连接,客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道
- 它们之间好比送信的线路和驿站的作用,比如要建议送信驿站,必须得了解送信的各个细节
TCP数据报结构

带阴影的几个字段需要重点说明一下:
(1) 序号Seq(Sequence Number)占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。
(2) 确认号Ack(Acknowledge Number) 确认号占32位,客户端和服务器端都可以发送,Ack = Seq + 1。
(3) 标志位: 每个标志位占用1Bit,共有6个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:
(1)URG:紧急指针(urgent pointer)有效
(2)ACK:确认序号有效
(3)PSH:接收方应该尽快将这个报文交给应用层
(4)RST:重置连接
(5)SYN:建立一个新连接
(6)FIN:断开一个连接
连接的建立(三次握手)
TCP建立连接时要传输三个数据包,俗称三次握手(Three-way Handshaking)。可以形象的比喻为下面的对话:
[Shake 1] 套接字A:“你好,套接字B,我这里有数据要传送给你,建立连接吧。”
[Shake 2] 套接字B:“好的,我这边已准备就绪。”
[Shake 3] 套接字A:“谢谢你受理我的请求。
客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求
这时客户端发起请求:
- 当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态。
- 服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包
- 服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。
- 服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充 确认号Ack 字段。
- 服务器将数据包发出,进入SYN-RECV状态
- 客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测 确认号Ack 字段,看它的值是否为 1000+1,如果是就说明连接建立成功。
- 接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充 确认号Ack 字段。
- 客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。
- 服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测确认号Ack 字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。
至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了。
TCP四次握手断开连接
建立连接非常重要,它是数据正确传输的前提;断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。
断开连接需要四次握手,可以形象的比喻为下面的对话:
[Shake 1] 套接字A:“任务处理完毕,我希望断开连接。”
[Shake 2] 套接字B:“哦,是吗?请稍等,我准备一下。”
等待片刻后……
[Shake 3] 套接字B:“我准备好了,可以断开连接了。”
[Shake 4] 套接字A:“好的,谢谢合作。”
下图演示了客户端主动断开连接的场景:
建立连接后,客户端和服务器都处于ESTABLISED状态。这时,客户端发起断开连接的请求:
- 客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。
- 服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。
注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。 - 客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。
- 等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态。
- 客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态。
- 服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态。
关于 TIME_WAIT 状态的说明
客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?
- TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。
- 客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。那么,要等待多久呢?
- 数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。这称为报文最大生存时间(MSL,Maximum Segment Lifetime)。TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包
优雅的断开连接 shutdown()
close()/closesocket()和shutdown()的区别
- 确切地说,close()/closesocket() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作。
- shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close()/ closesocket() 将套接字从内存清除。
- 调用 close()/closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。
- 默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会
TCP的粘包问题以及数据的无边界性
http://blog.csdn.net/binghuazh/archive/2009/05/28/4222516.aspx
Java URL处理
URL(Uniform Resource Locator)中文名为统一资源定位符,有时也被俗称为网页地址。表示为互联网上的资源,如网页或者FTP地址。
//URL可以分为如下几个部分
protocol(协议)可以是 HTTP、HTTPS、FTP 和 File,port 为端口号,path为文件路径及文件名
protocol://host:port/path?query#fragment
HTTP协议的URL实例如下
http://www.gdpi.com/index.html?language=cn#j2se
- 协议为(protocol):http
- 主机为(host:port):www.gdpi.com
- 端口号为(port): 80 ,以上URL实例并未指定端口,因为 HTTP 协议默认的端口号为 80。
- 文件路径为(path):/index.html
- 请求参数(query):language=cn
- 定位位置(fragment):j2se,定位到网页中 id 属性为 j2se 的 HTML 元素位置
URL 类方法
在java.net包中定义了URL类,该类用来处理有关URL的内容。对于URL类的创建和使用,下面分别进行介绍。
java.net.URL提供了丰富的URL构建方式,并可以通过java.net.URL来获取资源
| 序号 | 方法描述 |
|---|---|
| public URL(String protocol, String host, int port, String file) throws MalformedURLException. | 通过给定的参数(协议、主机名、端口号、文件名)创建URL。 |
| public URL(String protocol, String host, String file) throws MalformedURLException | 使用指定的协议、主机名、文件名创建URL,端口使用协议的默认端口。 |
| public URL(String url) throws MalformedURLException | 通过给定的URL字符串创建URL |
| public URL(URL context, String url) throws MalformedURLException | 使用基地址和相对URL创建 |
URL类中包含了很多方法用于访问URL的各个部分,具体方法及描述如下:
| 序号 | 方法描述 |
|---|---|
| public String getPath() | 返回URL路径部分 |
| public String getQuery() | 返回URL查询部分 |
| public String getAuthority() | 获取此 URL 的授权部分 |
| public int getPort() | 返回URL端口部 |
| public int getDefaultPort() | 返回协议的默认端口号 |
| public String getProtocol() | 返回URL的协议 |
| public String getHost() | 返回URL的主机 |
| public String getFile() | 返回URL文件名部分 |
| public String getRef() | 获取此 URL 的锚点(也称为”引用”) |
| public URLConnection openConnection() throws IOException | 打开一个URL连接,并运行客户端访问资源 |
以上实例演示了使用java.net的URL类获取URL的各个部分参数:
import java.net.*;
import java.io.*;
public class URLDemo {
public static void main(String[] args) {
try {
URL url = new URL("http://www.gdpi.com/index.html?language=cn#j2se");
System.out.println("URL 为:" + url.toString());
System.out.println("协议为:" + url.getProtocol());
System.out.println("验证信息:" + url.getAuthority());
System.out.println("文件名及请求参数:" + url.getFile());
System.out.println("主机名:" + url.getHost());
System.out.println("路径:" + url.getPath());
System.out.println("端口:" + url.getPort());
System.out.println("默认端口:" + url.getDefaultPort());
System.out.println("请求参数:" + url.getQuery());
System.out.println("定位位置:" + url.getRef());
} catch (IOException e) {
e.printStackTrace();
}
}
}
/*
URL 为:http://www.gdpi.com/index.html?language=cn#j2se
协议为:http
验证信息:www.gdpi.com
文件名及请求参数:/index.html?language=cn
主机名:www.gdpi.com
路径:/index.html
端口:-1
默认端口:80
请求参数:language=cn
定位位置:j2se
*/
URLConnections 类方法
openConnection() 返回一个 java.net.URLConnection。
例如:
- 如果你连接HTTP协议的URL, openConnection() 方法返回 HttpURLConnection 对象。
- 如果你连接的URL为一个 JAR 文件, openConnection() 方法将返回 JarURLConnection 对象。
- 等等…
URLConnection 方法列表如下:
| 序号 | 方法描述 |
|---|---|
| Object getContent() | 检索URL链接内容 |
| Object getContent(Class[] classes) | 检索URL链接内容 |
| String getContentEncoding() | 返回头部 content-encoding 字段值 |
| int getContentLength() | 返回头部 content-length字段值 |
| String getContentType() | 返回头部 content-type 字段值 |
| int getLastModified() | 返回头部 last-modified 字段值 |
| long getExpiration() | 返回头部 expires 字段值 |
| long getIfModifiedSince() | 返回对象的 ifModifiedSince 字段值 |
| public void setDoInput(boolean input) | URL 连接可用于输入和/或输出。如果打算使用 URL 连接进行输入,则将 DoInput 标志设置为 true;如果不打算使用,则设置为 false。默认值为 true |
| public void setDoOutput(boolean output) | URL 连接可用于输入和/或输出。如果打算使用 URL 连接进行输出,则将 DoOutput 标志设置为 true;如果不打算使用,则设置为 false。默认值为 false |
| public InputStream getInputStream() throws IOException | 返回URL的输入流,用于读取资源 |
| public OutputStream getOutputStream() throws IOException | 返回URL的输出流, 用于写入资源 |
| public URL getURL() | 返回 URLConnection 对象连接的URL |
以下实例中URL采用了HTTP 协议。 openConnection 返回HttpURLConnection对象。
import java.net.*;
import java.io.*;
public class URLConnDemo {
public static void main(String[] args) {
try {
URL url = new URL("http://www.baidu.com");
URLConnection urlConnection = url.openConnection();
HttpURLConnection connection = null;
if (urlConnection instanceof HttpURLConnection) {
connection = (HttpURLConnection) urlConnection;
} else {
System.out.println("请输入 URL 地址");
return;
}
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
BufferedWriter out = new BufferedWriter(new FileWriter("out.txt"));
String urlString = "";
String current;
while ((current = in.readLine()) != null) {
urlString += current;
}
System.out.println(urlString);
out.write(urlString);
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Java TCP通信程序
TCP通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)
两端通信时步骤:服务端程序,需要事先启动,等待客户端的连接,客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端
在Java中,提供了两个类用于实现TCP通信程序
客户端:java.net.Socket 类 表示创建Socket对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信
服务端:java.net.ServerSocket类 表示创建ServerSocket对象,相当于开启一个服务,并等待客户端的连接
Socket类
该类实现客户端套接字,套接字指的是两台设备之间通讯的端点
| 方法 | 说明 |
|---|---|
| public Socket(String host, int port) | 创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为回送地址 - 小贴士:回送地址(127.x.x.x) 是回环地址(Loopback Address) - 0.0.0.0才是真正表示“本网络中的本机” - 主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输 |
| public InputStream getInputStream() | 返回此套接字的输入流 - 如果此Scoket具有相关联的通道,则生成的InputStream 的所有操作也关联该通道。 - 关闭生成的InputStream也将关闭相关的Socket |
| public OutputStream getOutputStream() | 返回此套接字的输出流 - 如果此Scoket具有相关联的通道,则生成的OutputStream 的所有操作也关联该通道。 - 关闭生成的OutputStream也将关闭相关的Socket |
| public void close() | 关闭此套接字 - 一旦一个socket被关闭,它不可再使用。 - 关闭此socket也将关闭相关的InputStream和OutputStream |
| public void shutdownOutput() | 禁用此套接字的输出流 任何先前写出的数据将被发送,随后终止输出流 |

ServerSocket类
这个类实现了服务器套接字,该对象等待通过网络的请求
| 方法 | 说明 |
|---|---|
| public ServerSocket(int port) | 使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号 |
| public Socket accept() | 侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接 |

实现上传文件
Server
Client
创建BS版本TCP服务器
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
// 创建BS版本TCP服务器
public class TCPServer {
public static void main(String[] args) throws IOException {
//1.创建一个服务器ServerSocket,和系统要指定的端口号
ServerSocket server = new ServerSocket(8888);
//浏览器解析非.html文件,会单独开启一个线程,读取其他文件,我们也要让服务器一直处于监听状态和开启多线程
while (true) {
//2.使用accept方法获取到请求的客户端对象(浏览器)
Socket socket = server.accept();
new Thread(() -> {
try {
//3.使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// while (len(is.read())!=-1)输出的浏览的对象中返回的参数
//GET /web/index.html HTTP/1.1
//Host: 127.0.0.1:8888
//Connection: keep-alive
//Upgrade-Insecure-Requests: 1
//User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64)
//Accept: text/html,application/xhtml+xml,application
//Accept-Encoding: gzip, deflate, br
//Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
//4.把is网络字节输入流对象,转换为字符缓冲输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//5.把客户端请求信息的第一行GET /web/index.html HTTP/1.1 获取(web/index.html)
String line = br.readLine();
String[] arr = line.split(" ");
String htmlpath = arr[1].substring(1);
//6.使用Socket中的方法getOutputStream获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
//7.写入HTTP协议响应头,固定写法
os.write("HTTP/1.1 200 OK\r\n".getBytes());
os.write("Content-Type:text/html\r\n".getBytes());
os.write("\r\n".getBytes());
//8.创建一个本地字节输入流,构造方法中绑定要读取的html路径,把服务读取的html文件回写到客户端
FileInputStream fis = new FileInputStream(htmlpath);
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) os.write(bytes, 0, len);
//9.释放资源
fis.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
