上一篇:15【IO流增强】
下一篇:17【测试单元、反射、注解、Lombok插件】
目录:【JavaSE零基础系列教程目录】
文章目录
- 16【TCP、UDP、网络通信】
- 一、基于网络编程
- 1.1 网络编程概述
- 1.2 网络分层
- 1.2.1 OSI参考模型
- 1.2.2 TCP/IP参考模型
- 1.3 网络名词
- 1.3.1 IP地址
- 1.3.2 端口号
- 1.4 网络协议
- 1.4.1 UDP
- 1)UDP传输过程
- 2)UDP报文格式
- 3)UDP总结
- 1.4.2 TCP
- 1)TCP报文格式
- 2)三次握手
- 3)四次挥手
- 4)TCP总结
- 二、Java实现UDP应用程序
- 2.1 InetAddress类
- 2.2 DatagramPacket类
- 2.2.1 构造方法
- 2.2.2 常用方法
- 2.2 DatagramSocket类
- 2.2.1 构造方法
- 2.2.2 常用方法
- 2.3 UDP实现数据发送与接收
- 三、Java实现TCP程序
- 2.1 Socket
- 2.1.1 构造方法
- 2.1.2 成员方法
- 2.2 ServerSocket
- 2.2.1 构造方法
- 2.2.2 成员方法
- 2.3 设计TCP应用程序
- 四、综合案例
- 4.1 TCP在线聊天案例
- 4.2 TCP图片上传案例
- 4.2.1 简单图片上传
- 4.2.2 使用多线程改进
16【TCP、UDP、网络通信】
一、基于网络编程
1.1 网络编程概述
计算机网络是通过传输介质(网线)、通信设施(路由器、交换机等)和网络通信协议,把分散在不同地点的计算机设备互连起来的,用来实现数据共享。
网络编程就是编写程序使互联网的多个设备(如计算机)之间进行数据传输。Java语言对网络编程提供了良好的支持。通过其提供的接口我们可以很方便地进行网络编程。
1.2 网络分层
通过网络发送数据是一项复杂的操作,通过网络将数据从一台主机发送到另外的主机,这个过程是通过计算机网络通信来完成。
网络通信的不同方面被分解为多个层,层与层之间用接口连接。通信的双方具有相同的层次,层次实现的功能由协议数据单元来描述。不同系统中的同一层构成对等层,对等层之间通过对等层协议进行通信,理解批次定义好的规则和约定。将网络分层,这样就可以修改甚至替换某一层的软件,只要层与层之间的接口保持不变,就不会影响到其他层。
1.2.1 OSI参考模型
世界上第一个网络体系结构在1974年由IBM公司提出,名为SNA。以后其他公司也相继提出自己的网络体系结构。为了促进计算机网络的发展,国际标准化组织ISO在现有网络的基础上,提出了不基于具体机型、操作系统或公司的网络体系结构,称为开放系统互连参考模型,即OSI/RM(Open System Interconnection Reference Model)。
- 物理层:为数据端设备提供原始比特流的传输的通路;网络通信的数据传输介质,由电缆与设备共同构成。常见:中继器,集线器(HUB)、网线、RJ-45标准等
- 数据链路层:在通信的实体间建立数据链路连接;将数据分帧,并处理流控制、物理地址寻址、重发等。常见:网卡,网桥,二层交换机等
- 网络层:为数据在结点之间传输创建逻辑链路,并分组转发数据;对子网间的数据包进行路由选择。常见:路由器、多层交换机、防火墙、IP、IPX、RIP、OSPF
- 传输层:提供应用进程之间的逻辑通信;建立连接,处理数据包错误、数据包次序。常见:TCP、UDP、SPX、进程、端口(socket)
- 会话层:建立端连接并提供访问验证和会话管理;为数据在结点之间传输创建逻辑链路,并分组转发数据;使用校验点可使会话在通信失效时从校验点恢复通信。常见:服务器验证用户登录、断点续传
- 表示层:提供数据格式转换服务;解密与加密,图片解码和编码、数据的压缩和解压缩。常见:URL加密、口令加密、图片编解码
- 应用层:访问网络服务的接口;为操作系统或网络应用程序提供访问网络服务的接口。常见:Telnet、FTP、 HTTP、 SNMP、DNS等
1.2.2 TCP/IP参考模型
TCP/IP,即Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,是Internet最基本的协议,Internet国际互联网络的基础。
TCP/IP协议是一个开放的网络协议簇,它的名字主要取自最重要的网络层IP协议和传输层TCP协议。TCP/IP协议定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。TCP/IP参考模型采用4层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求,这4个层次分别是:网络接口层、网络层(IP层)、传输层(TCP层)、应用层。
1.3 网络名词
1.3.1 IP地址
IP(Internet Protocol Address):全称互联网协议地址,简称IP地址;IP地址用于标识互联网上的唯一一台机器,互联网就是通过IP地址锁定到我们的这台电脑,相当于家庭地址;
IP地址的分类
-
IPv4
:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d
的形式,例如192.168.65.100
。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。 -
IPv6
:由于互联网的网民日益增多,IPv4的IP地址资源有限。为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
- 查看本机IP地址,在cmd控制台输入:
ipconfig
- 检查网络是否连通,在cmd控制台输入:
ping 空格 IP地址
ping 163.177.151.109
tips:在每台计算机出厂时,都有一个用于标识自己电脑的地址:
127.0.0.1
,和一个本机域名:localhost
1.3.2 端口号
在两台计算机通信时,更准确的来说是两台计算机的某个进程(应用程序)在通信,IP地址可以唯一标识网络中的设备,那么端口号就是唯一标识计算机中的某个应用程序了;
tips:端口号的取值范围为065536。其中01023之间的端口号用于计算机内置的一些进程,我们自己的程序的端口号尽量设置在1023以上的端口,保证端口不会占用冲突;
1.4 网络协议
如同人与人之间相互交流是需要遵循一定的规则(如语言)一样,计算机之间能够进行相互通信是因为它们都共同遵守一定的规则,即网络协议。
OSI参考模型和TCP/IP模型在不同的层次中有许多不同的网络协议,如图所示:
我们今天主要讨论的是传输层的协议,即考虑应用程序之间的逻辑通信。简单来说就是数据该如何发送给其他机器;
1.4.1 UDP
UDP(User Datagram Protocol):用户数据报协议;UDP是面向无连接的通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
1)UDP传输过程
UDP是面向报文传递数据的;在UDP传输过程中,分别为发送端和接受端;
发送端使用UDP发送数据时,首先将其包裹成一个UDP报文(包含数据与首部格式)通过网络将其发送给接收端;接受端接收到UDP报文后,首先去掉其首部,将数据部分交给应用程序进行解析;
需要注意的是,UDP不保证数据传递的可靠性,在传递过程中可能出现丢包等情况,另外,即使接收方不存在报文依旧被发送出去(丢包)。但正是因为UDP不需要花费额外的资源来建立可靠的连接,因此UDP传输速度快,资源消耗小;
2)UDP报文格式
一个完整的UDP报文包含首部和载荷(数据)两部分,首部由 4 个 16 位(2 字节)长的字段,共8个字节组成,分别说明该报文的源端口、目的端口、报文长度和校验值。
UDP 报文中每个字段的含义如下:
- 源端口:发送端所使用应用程序的UDP端口,接受端的应用程序理由这个字段的值作为响应的目的地址;这个字段是可选的,所以发送端的应用程序不一定会把自己的端口号写入该字段中。如果不写入端口号,则把这个字段设置为 0。这样,接收端的应用程序就不能发送响应了。
- 目的端口:接收端计算机上 UDP 软件使用的端口。
- 长度:表示 UDP 数据报长度,单位:字节;包含 UDP 报文头+UDP 数据长度。因为 UDP 报文头长度是 8 个字节,所以这个值最小为 8。
- 校验值:可以检验数据在传输过程中是否被损坏。
3)UDP总结
由于使用UDP协议消耗资源小,通信效率高;因此一般用于实时性要求比较高的场合如:音频、视频的传输等;例如视频会议都使用UDP协议,如果出现了网络丢包情况也只是造成卡帧现象,对整体影响不大
但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。UDP的交换过程如下图所示。
1.4.2 TCP
TCP(Transmission Control Protocol):传输控制协议;TCP协议是面向连接的通信协议;即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
在TCP连接中必须要明确客户端(发送端)与服务器端(接收端),由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”;
1)TCP报文格式
一个完整的TCP报文同样也是由首部和数据载荷组成;TCP的全部功能都体现在它首部中各字段的作用。
- 源端口:占2个字节,16个比特;表示发送该报文的应用程序端口号,用于接收端的响应;
- 目的端口号:占2个字节,16个比特;标识接受该TCP报文的应用程序端口号;
- 序号:数据载荷中的数据都是有顺序的,序号用于标识发送端向接收端发送的数据字节流的位置;
序号占4个字节,32个比特位,取值范围
2^32-1
,序号增加到最后一个时,下一个序列号又回到0;
- 确认号:期望收到对方下一个TCP报文序号的起始位置,同时也是对之前收到的数据进行确认;
确认号和序号一样,占4个字节,32个比特位,取值范围
2^32-1
,确认号增加到最后一个时,下一个确认号又回到0;
A向B发送数据:
B向A发送数据:
若确认号=n,则表明,序号n-1为止的所有数据都已正确接收,期望接收序号为n的数据;
- 本次的序列号:上次的确认号
- 本次的确认号:上次的序列号+1
- 数据偏移:占4个比特,用来指出数据载荷部分的起始处距离报文的起始处有多远;也就是TCP首部的长度。需要注意的是数据偏移以4个字节为单位;
如图:
- 保留字段:占6个比特,保留为今后使用,但目前为0;
- 窗口: 占2个字节,16个比特;用于流量控制和拥塞控制,表示当前接收缓冲区的大小。在计算机网络中,通常是用接收方的接收能力的大小来控制发送方的数据发送量。TCP连接的一端根据缓冲区大小确定自己的接收窗口值,告诉对方,使对方可以确定发送数据的字节数。
- 校验和:占2个字节,16比特;检查报文的首部和数据载荷两部分,底层依赖于具体的校验算法;
- 紧急指针:占2个字节,16比特;用来指明紧急数据的长度;当发送端有紧急数据时,可将紧急数据插队到发送缓存的最前面,并立刻封装到一个TCP报文段中进行发送。紧急指针会指出本报文段数据载荷部分包含了多长的紧急数据,紧急数据之后是普通数据
- 选项:附加一些额外的首部信息;
- 填充:由于选项的长度可变,因此用来填充的确认报文首部能被4整除(因为数据偏移字段,也就是首部长度字段,是以4字节为单位的);
- 标志位:
- ACK(确认):取值为1时确认号字段才有效;取值为0时确认号字段无效,一般情况下都为1;
- SYN(同步):在连接建立时用来同步序号
- FIN(终止):为1时表明发送端数据发送完毕要求释放连接
- RST(复位):用于复位TCP连接,值为1时说明连接出现了异常,必须释放连接,然后再重新建立连接,有时RST置1还用来拒绝一个非法的报文段或拒绝打开一个TCP连接;
- PSH(推送):为1时接收方应尽快将这个报文交给应用层,而不必等到接受缓存都填满后再向上交付
- URG(紧急):为1时表明紧急指针字段有效,取值为0时紧急字段无效;
2)三次握手
由于TCP是基于可靠通信的,在发送数据之前必须建立可靠的连接;TCP建立连接的过程分为三个步骤,我们称为"三次握手";
简单的过程如下图所示:
- 1)第一次握手:发送端向接收端端发出连接请求,等待接受的响应。
- 2)第二次握手,接收端向发送端响应,通知发送端收到了连接请求。
- 3)第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示。
我们结合TCP报文原理来具体分析一下三次握手的详细流程:
原理图如下:
- 1)第一次握手:首先A使用TCP连接向B发送报文(此报文不携带载荷,只有首部),报文中的SYN记为1(如果发送失败,重新发送记为2),序号记为x
- 2)第二次握手:B接受到A发送的报文后,给A发送一个确认报文(该报文也仅有首部),报文中SYN和ACK都取值为1,用于标志这是一个确认报文段;同时此次响应的序号记为y,确认号为上一次序号x加1的值,表示已经成功接收到x+1之前的所有内容,下一次开始接受x+1之后的数据(包含x+1的数据);
- 3)第三次握手:A再次向B发送TCP确认,序号为x+1,代表发送序号x之后的数据;确认号为y+1,代表已经接收了来自B的响应的全部数据;
3)四次挥手
TCP建立连接时需要"三次握手",断开连接时则需要"四次挥手";
原理图如下:
- 1)A向B发送连接释放请求,标记FIN=1(表示需要断开连接);序号记为u(u为之前已发送的最后一个字节数据的序号+1),确认号记为v(v为之前已接受的最后一个字节数据的序号+1)
- 2)B收到A发送的断开连接请求后开始响应,标记序号为v,确认号为u+1(代表已经成功接收A发送的数据,接下来开始接受u+1及之后序号的数据)
- 3)主机B如果仍有数据需要传输依旧可以传输,如果B没有数据需要发送时,需给A发送连接释放报文。标记FIN=1,序号为w(同理,此时w是B之前已发送最后一个字节数的序号+1),确认号为u+1;
- 4)A接收到B发送的连接释放报文后,最终给B发送一个TCP确认报文,代表确定接受到B需要连接断开。序号为u+1,确认号为w+1(代表B发送的数据以及全部接受完毕)
4)TCP总结
使用TCP协议传输数据时,必须要建立可靠连接(三次握手),当连接关闭时还需要四次挥手,对性能损耗较大,如果频繁的创建和关闭TCP连接性能势必会有很大影响。但是由于TCP的可靠传输,对一些数据完整性要求较高的场合比较适用,如文件上传下载等;
二、Java实现UDP应用程序
2.1 InetAddress类
java.net.InteAddress
类是用于描述IP地址和域名的一个Java类;
常用方法如下:
-
public static InetAddress getByName(String host)
:根据主机名获取InetAddress对象 -
public String getHostName()
:获取该对象对应的主机名 -
public String getHostAddress()
获取该对象对应的IP地址
示例代码:
package com.dfbz.demo01;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) throws UnknownHostException {
// 根据域名获取InetAddress对象(获取本机对象)
InetAddress t1 = InetAddress.getByName("localhost");
System.out.println(t1.getHostName()); // localhost
System.out.println(t1.getHostAddress()); // 127.0.0.1
System.out.println("------------------------");
// 根据域名获取InetAddress对象(获取百度的InetAddress对象)
InetAddress t2 = InetAddress.getByName("www.baidu.com");
System.out.println(t2.getHostName()); // www.baidu.com
System.out.println(t2.getHostAddress()); // 163.177.151.109
}
}
2.2 DatagramPacket类
java.net.DatagramPacket
类用于封装一个UDP数据报文
2.2.1 构造方法
-
public DatagramPacket(byte[] buf, int length, InetAddress address, int port)
:创建一个数据包对象
-
buf
:要发送的内容 -
length
:要发送的内容⻓度,单位字节 -
address
:接收端的ip地址 -
port
:接收端⼝号
-
public DatagramPacket(byte buf[], int length)
:创建一个数据包对象
示例代码:
package com.dfbz.demo01;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02 {
public static void main(String[] args) throws UnknownHostException {
byte[] data1 = "hello~".getBytes();
DatagramPacket packet1 = new DatagramPacket(data1, data1.length);
byte[] data2 = "你好".getBytes();
InetAddress address = InetAddress.getByName("localhost");
DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, 6868);
}
}
2.2.2 常用方法
-
public synchronized int getLength()
:获取此UDP数据包载荷的数据长度(单位字节) -
public synchronized int getPort()
:获取此UDP数据包的目的端口号 -
public synchronized byte[] getData()
:获取此UDP数据包的载荷部分(数据)
示例代码:
package com.dfbz.demo01;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo03 {
public static void main(String[] args) throws UnknownHostException {
byte[] data = "hello".getBytes();
DatagramPacket packet = new DatagramPacket(
data,
data.length,
InetAddress.getByName("localhost"),
6868
);
System.out.println(packet.getLength()); // 5
System.out.println(packet.getPort()); // 6868
System.out.println(new String(packet.getData())); // hello
}
}
2.2 DatagramSocket类
java.net.DatagramSocket
类用于描述一个UDP发送端或接收端;
2.2.1 构造方法
-
public DatagramSocket(int port)
:通过端口构建一个发送端/接收端
示例代码:
DatagramSocket socket = new DatagramSocket(6969);
2.2.2 常用方法
-
public void send(DatagramPacket p)
:发送一个UDP数据包 -
public synchronized void receive(DatagramPacket p)
:接收一个UDP数据包 -
public void close()
:释放该Socket占用的资源
2.3 UDP实现数据发送与接收
- 发送端:
package com.dfbz.demo01;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* @author lscl
* @version 1.0
* @intro: 发送端
*/
public class Demo04 {
public static void main(String[] args) throws Exception {
String str = "你好";
// 准备一个UDP数据包
DatagramPacket packet = new DatagramPacket(
str.getBytes(),
str.getBytes().length,
InetAddress.getLocalHost(),
6868);
// 套接字
DatagramSocket socket = new DatagramSocket();
// 发送数据包
socket.send(packet);
socket.close();
}
}
- 接收端:
package com.dfbz.demo01;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* @author lscl
* @version 1.0
* @intro: 接收端
*/
public class Demo05 {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(666);
byte[] bytes = new byte[1024];
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
//接受数据字节的长度
System.out.println("接收端端:");
// 接受一个UDP数据包
socket.receive(packet);
int len = packet.getLength();
System.out.println("已经接收到:" + len + "个字节");
// 转换为字符串打印
System.out.println(new String(bytes, 0, len));
socket.close();
}
}
三、Java实现TCP程序
在TCP通信中,分为数据的发送端(客户端)和接收端(服务器),当建立连接成功后(三次握手),才可以进行数据的发送;
在Java中,提供了两个类用于实现TCP通信程序:
- 1)客户端:
java.net.Socket
类表示;用于与服务器端建立连接,向服务器端发送数据报文等; - 2)服务端:
java.net.ServerSocket
类表示;用于与客户端的交互;
2.1 Socket
java.net.Sokcet
用于封装一个TCP应用程序的客户端;
2.1.1 构造方法
-
public Socket(String host, int port)
:创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为本机地址。
示例代码:
Socket client = new Socket("127.0.0.1", 6868);
2.1.2 成员方法
-
public InputStream getInputStream()
: 返回此套接字的输入流。关闭生成的InputStream也将关闭相关的Socket。 -
public OutputStream getOutputStream()
: 返回此套接字的输出流。关闭生成的OutputStream也将关闭相关的Socket。 -
public void close()
:关闭此套接字。关闭此socket也将关闭相关的InputStream和OutputStream 。 -
public void shutdownOutput()
: 禁用此套接字的输出流。任何先前写出的数据将被发送,随后终止输出流。
2.2 ServerSocket
ServerSocket
类:这个类实现了服务器套接字,该对象等待通过网络的请求。
2.2.1 构造方法
-
public ServerSocket(int port)
:使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。
构造举例,代码如下:
ServerSocket server = new ServerSocket(6666);
2.2.2 成员方法
-
public Socket accept()
:监听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。
2.3 设计TCP应用程序
- 客户端代码:
package com.dfbz.demo01;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* @author lscl
* @version 1.0
* @intro: 客户端程序
*/
public class Demo01_Client {
public static void main(String[] args) throws IOException {
// 创建一个客户端,指定要连接服务器的IP与端口
Socket socket = new Socket("127.0.0.1", 6969);
// 获取与此服务器的输入流(用于读取该服务器的数据)
InputStream is = socket.getInputStream();
// 获取与此服务器的输出流(用于向该服务器发送数据)
OutputStream os = socket.getOutputStream();
// 向服务器发送数据
os.write("在吗?".getBytes());
// 准备一个字节数组用于接收数据
byte[] bytes = new byte[1024];
// 读取服务器发送过来的数据(若服务器一直未发送数据,则程序阻塞在此)
int len = is.read(bytes);
System.out.println(new String(bytes, 0, len));
// 向服务器写出数据
os.write("买糖".getBytes());
// 读取服务器的数据
len = is.read(bytes);
System.out.println(new String(bytes, 0, len));
// 释放连接资源
socket.close();
}
}
- 服务端代码:
package com.dfbz.demo01;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author lscl
* @version 1.0
* @intro: 服务端程序
*/
public class Demo02_Server {
public static void main(String[] args) throws IOException {
// 创建一个服务器,并指定该TCP程序的端口(IP地址就是本机)
ServerSocket serverSocket=new ServerSocket(6969);
System.out.println("等待客户端: ");
// 接收一个客户端(若没有客户端来连接服务器,则程序阻塞在此)
Socket client = serverSocket.accept();
// 获取与客户端的输入流(用于读取客户端的数据)
InputStream is = client.getInputStream();
// 获取与客户端的输出流(用于向客户端发送数据)
OutputStream os = client.getOutputStream();
byte[] bytes=new byte[1024];
// 读取客户端发送过来的数据
int len = is.read(bytes);
System.out.println(new String(bytes,0,len));
// 向客户端写出数据
os.write("在哦!亲~,想买点什么?".getBytes());
// 读取客户端的数据
len = is.read(bytes);
System.out.println(new String(bytes,0,len));
// 向客户端写出数据
os.write("糖没了,不卖!".getBytes());
client.close();
serverSocket.close();
}
}
上述程序中,发送内容都是写死在代码中,我们使用Scanner来接受键盘录入的数据进行发送;
- 改造客户端程序:
package com.dfbz.demo02;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @author lscl
* @version 1.0
* @intro: 客户端程序
*/
public class Demo01_Client {
public static void main(String[] args) throws IOException {
// 创建一个客户端,指定要连接服务器的IP与端口
Socket socket = new Socket("127.0.0.1", 6969);
// 获取与此服务器的输入流(用于读取该服务器的数据)
InputStream is = socket.getInputStream();
// 获取与此服务器的输出流(用于向该服务器发送数据)
OutputStream os = socket.getOutputStream();
// 获取scanner扫描器
Scanner scanner = new Scanner(System.in);
// 死循环进行发送
while (true) {
System.out.println("请输入要发送给服务器的信息: ");
String sendInfo = scanner.next();
// 如果输入end代表程序结束
if ("end".equals(sendInfo)) {
System.out.println("程序结束...");
break;
}
// 向服务器发送数据
os.write(sendInfo.getBytes());
// 准备一个字节数组用于接收数据
byte[] bytes = new byte[1024];
// 读取服务器发送过来的数据(若服务器一直未发送数据,则程序阻塞在此)
int len = is.read(bytes);
System.out.println("接收到来自服务器" + socket.getInetAddress().getHostAddress() + "的信息: " + new String(bytes, 0, len));
}
// 释放连接资源
socket.close();
}
}
- 改造服务端程序:
package com.dfbz.demo02;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
/**
* @author lscl
* @version 1.0
* @intro: 服务端程序
*/
public class Demo02_Server {
public static void main(String[] args) throws IOException {
// 创建一个服务器,并指定该TCP程序的端口(IP地址就是本机)
ServerSocket serverSocket = new ServerSocket(6969);
System.out.println("等待客户端: ");
// 接收一个客户端(若没有客户端来连接服务器,则程序阻塞在此)
Socket client = serverSocket.accept();
// 获取与客户端的输入流(用于读取客户端的数据)
InputStream is = client.getInputStream();
// 获取与客户端的输出流(用于向客户端发送数据)
OutputStream os = client.getOutputStream();
Scanner scanner = new Scanner(System.in);
while (true) {
byte[] bytes = new byte[1024];
// 读取客户端发送过来的数据
int len = is.read(bytes);
String receiveInfo = new String(bytes, 0, len);
// 如果客户端发送过来的是end则程序结束
if ("end".equals(receiveInfo)) {
System.out.println("程序结束...");
break;
}
System.out.println("接受到来自客户端" + client.getInetAddress().getHostAddress() + "的信息: " + receiveInfo);
String sendInfo = scanner.next();
// 向客户端写出数据
os.write(sendInfo.getBytes());
}
client.close();
serverSocket.close();
}
}
四、综合案例
4.1 TCP在线聊天案例
我们刚刚使用了TCP完成了聊天功能的编写;我们会发现我们的程序是由问题的,就是读写是串行的!
我们整个应用程序只有一个线程,那就mian线程,代码都是从上往下执行,如果main线程当前在读操作,那么就不能写。而且如果此时一方如果没有发送信息给另一方,那么另一方的read方法将会一直处于阻塞状态,代码不会往下执行;此时想往对方写出数据肯定是不行的;
我们利用多线程技术来改造我们之前的代码,让我们的代码既可以一直读,又可以一直写;
- 多线程改进客户端:
package com.dfbz.demo01;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* @author lscl
* @version 1.0
* @intro: 多线程实现客户端
*/
public class Demo01_Client {
public static void main(String[] args) throws Exception {
// 获取到一个新的客户端
Socket socket = new Socket("127.0.0.1", 6969);
// 读取客户端数据
InputStream is = socket.getInputStream();
// 缓冲字符输入流(读取客户端数据更加方便)
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 读线程
new Thread() {
@Override
public void run() {
try {
String info;
while (true) {
// 死循环读取客户端的信息
info = br.readLine();
if (info.equals("end")) {
socket.close();
// 退出应用程序
System.exit(0);
System.out.println("bye bye....");
break;
} else {
System.out.println("接收到: " + socket.getInetAddress().getHostAddress() + "来自的消息: " + info);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
// 给客户端写回数据
OutputStream os = socket.getOutputStream();
// 缓冲字符输出流(往客户端写数据更加方便)
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
// 写线程
new Thread() {
@Override
public void run() {
try {
Scanner scanner = new Scanner(System.in);
String info;
while (true) {
// 获取键盘输入的信息
info = scanner.nextLine();
// 往客户端写数据
bw.write(info);
bw.newLine();
bw.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
- 多线程改进服务器:
package com.dfbz.demo01;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
/**
* @author lscl
* @version 1.0
* @intro: 多线程实现服务器
*/
public class Demo02_Server {
public static void main(String[] args) throws Exception {
// 创建一台服务器
ServerSocket serverSocket = new ServerSocket(6969);
while (true) {
// 获取到一个新的客户端
Socket socket = serverSocket.accept();
System.out.println(socket.getInetAddress().getHostAddress() + "已与您连接成功");
// 读取客户端数据
InputStream is = socket.getInputStream();
// 缓冲字符输入流(读取客户端数据更加方便)
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 读线程
new Thread() {
@Override
public void run() {
try {
String info;
while (true) {
// 死循环读取客户端的信息
info = br.readLine();
if (info.equals("end")) {
// 关闭与客户端的连接
socket.close();
System.out.println(socket.getInetAddress().getHostAddress() + "已与您断开连接: ");
break;
} else {
System.out.println("接收到: " + socket.getInetAddress().getHostAddress() + "来自的消息: " + info);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
// 给客户端写回数据
OutputStream os = socket.getOutputStream();
// 缓冲字符输出流(往客户端写数据更加方便)
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
// 写线程
new Thread() {
@Override
public void run() {
try {
Scanner scanner = new Scanner(System.in);
String info;
while (true) {
// 获取键盘输入的信息
info = scanner.nextLine();
// 往客户端写数据
bw.write(info);
bw.newLine();
bw.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
4.2 TCP图片上传案例
4.2.1 简单图片上传
图片上传流程:
1)客户端首先通过输入流将自己磁盘中的图片读取到内存中
2)客户端通过TCP连接的输出流,向服务器写出刚刚读取到的图片数据
3)服务器通过TCP连接的输入流,将客户端刚刚发送过来的图片数据读取到内存中
4)服务器通过输出流将内存中的数据写入到服务器的磁盘中
tips:我们学习过程中,将服务器和客户端放在同一台机器。但实际开发中服务器和客户端不是在同一台机器,
图片上传代码实现:
- 服务器代码实现:
package com.dfbz.demo02;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01_Server {
public static void main(String[] args) throws Exception {
// 声明服务器
ServerSocket serverSocket = new ServerSocket(8888);
// 接收到一个客户端
Socket client = serverSocket.accept();
System.out.println(client.getInetAddress().getHostAddress() + "连接成功");
// 读取客户端传递过来的数据
InputStream is = client.getInputStream();
// 随机生成一个文件名写出到磁盘
FileOutputStream fos = new FileOutputStream(UUID.randomUUID().toString() + ".png");
byte[] data = new byte[1024];
int len;
while ((len = is.read(data)) != -1) {
// 写出到磁盘
fos.write(data, 0, len);
}
// 释放资源
fos.close();
client.close();
}
}
- 客户端代码实现:
package com.dfbz.demo02;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02_Client {
public static void main(String[] args) throws Exception{
Socket socket = new Socket("localhost", 8888);
// 读取磁盘中的图片
FileInputStream fis = new FileInputStream("C:\\Users\\Horizon\\Desktop\\001.png");
// 往服务器传递数据
OutputStream os = socket.getOutputStream();
byte[] data=new byte[1024];
int len;
while ((len=fis.read(data))!=-1){
// 写出到服务器端
os.write(data,0,len);
}
fis.close();
socket.close();
}
}
4.2.2 使用多线程改进
实际开发中一个服务器对应N多个客户端,其他客户端均可以上传图片。我们的代码在同一时间只允许一个人上传图片,如果这个人上传的文件较大,那么势必会造成其他用户处于等待状态;针对这种情况我们可以使用多线程来解决。
服务器每次接受到一个客户端时,都开启一个线程来独立处理这个客户端的上传任务。这样在很多人同时来上传文件时,都可以一起上传。
- 多线程改进服务器:
package com.dfbz.demo03;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author lscl
* @version 1.0
* @intro: 多线程改进图片上传服务器
*/
public class Demo01_Server {
public static void main(String[] args) throws Exception {
// 声明服务器
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
// 接收到一个客户端
Socket socket = serverSocket.accept();
// 每接收到一个客户端都开启一个独立的线程为该客户端提供服务
new Thread(){
@Override
public void run() {
System.out.println(socket.getInetAddress().getHostAddress() + "连接成功");
try {
// 读取客户端传递过来的数据
InputStream is = socket.getInputStream();
// 写出到文件
FileOutputStream fos = new FileOutputStream(UUID.randomUUID().toString() + ".png");
byte[] data = new byte[1024];
int len;
while ((len = is.read(data)) != -1) {
// 写出到磁盘
fos.write(data, 0, len);
}
System.out.println("文件上传成功!");
// 释放资源
fos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
上一篇:15【转换流、缓冲流、序列流、打印流】
下一篇:17【测试单元、反射、注解、Lombok插件】
目录:【JavaSE零基础系列教程目录】