文章目录
一、概述
TCP通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。
-
两端通信时步骤:
- 服务端程序,需要事先启动,等待客户端的连接。
- 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
-
在Java中,提供了两个类用于实现TCP通信程序:
- 客户端:
java.net.Socket
类表示。创建Socket
对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。 - 服务端:
java.net.ServerSocket
类表示。创建ServerSocket
对象,相当于开启一个服务,并等待客户端的连接。
- 客户端:
二、Socket类
TCP通信的客户端:向服务器发送连接请求,给服务器发送数据,读取服务器回写的数据
Socket
类:此类实现客户端套接字(也可以就叫“套接字”)。- 套接字是两台机器间通信的端点。
- 套接字:包含了 IP 地址和端口号的网络单位 。
2.1 构造方法
-
public Socket(String host, int port)
:创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为回送地址。小贴士:回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。
构造举例,代码如下:
Socket client = new Socket("127.0.0.1", 8888);
2.2 成员方法
public InputStream getInputStream()
: 返回此套接字的输入流。- 如果此
Scoket
具有相关联的通道,则生成的InputStream
的所有操作也关联该通道。 - 关闭生成的
InputStream
也将关闭相关的Socket
。
- 如果此
public OutputStream getOutputStream()
: 返回此套接字的输出流。- 如果此
Scoket
具有相关联的通道,则生成的OutputStream
的所有操作也关联该通道。 - 关闭生成的
OutputStream
也将关闭相关的Socket。
- 如果此
public void close()
:关闭此套接字。- 一旦一个
socket
被关闭,它不可再使用。 - 关闭此
socket
也将关闭相关的InputStream
和OutputStream
。
- 一旦一个
public void shutdownOutput()
: 禁用此套接字的输出流。- 任何先前写出的数据将被发送,随后终止输出流。
2.3 实现步骤
- 创建一个客户端对象
Socket
,构造方法绑定服务器的IP地址和端口号 - 使用
Socket
对象中的方法getOutputStream()
获取网络字节输出流OutputStream
对象 - 使用网络字节输出流
OutputStream
对象中的方法write
,给服务器发送数据 - 使用
Socket
对象中的方法getInputStream()
获取网络字节输入流InputStream
对象 - 使用网络字节输入流
InputStream
对象中的方法read
,读取服务器回写的数据 - 释放资源 (Socket)
注意:
- 客户端和服务器端进行交互,必须使用
Socket
中提供的网络流,不能使用自己创建的流对象- 当我们创建客户端对象
Socket
的时候,就会去请求服务器,和服务器经过3次握手建立连接通路
- 这时如果服务器没有启动,那么就会抛出异常
ConnectException: Connection refused: connect
- 如果服务器已经启动,那么就可以进行交互了
三、ServerSocket类
TCP通信的服务器端:接收客户端的请求,读取客户端发送的数据,给客户端回写数据
ServerSocket
类:这个类实现了服务器套接字,该对象等待通过网络的请求。
3.1 构造方法
-
public ServerSocket(int port)
:使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。构造举例,代码如下:
ServerSocket server = new ServerSocket(8888);
3.2 成员方法
服务器端必须明确一件事情,必须的知道是哪个客户端请求的服务器,所以可以使用 accept
方法获取到请求的客户端对象 Socket
public Socket accept()
:侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方***一直阻塞直到建立连接。
3.3 实现步骤
- 创建服务器
ServerSocket
对象和系统要指定的端口号 - 使用
ServerSocket
对象中的方法accept
,获取到请求的客户端对象Socket
- 使用
Socket
对象中的方法getInputStream()
获取网络字节输入流InputStream
对象 - 使用网络字节输入流
InputStream
对象中的方法read
,读取客户端发送的数据 - 使用
Socket
对象中的方法getOutputStream()
获取网络字节输出流OutputStream
对象 - 使用网络字节输出流
OutputStream
对象中的方法write
,给客户端回写数据 - 释放资源(Socket,ServerSocket)
四、简单的TCP网络程序
4.1 TCP通信分析图解
- 【服务端】启动,创建
ServerSocket
对象,等待连接。 - 【客户端】启动,创建
Socket
对象,请求连接。 - 【服务端】接收连接,调用
accept
方法,并返回一个Socket
对象。 - 【客户端】
Socket
对象,获取OutputStream
,向服务端写出数据。 - 【服务端】
Scoket
对象,获取InputStream
,读取客户端发送的数据。
到此,客户端向服务端发送数据成功。
自此,服务端向客户端回写数据。
- 【服务端】
Socket
对象,获取OutputStream
,向客户端回写数据。 - 【客户端】
Scoket
对象,获取InputStream
,解析回写数据。 - 【客户端】释放资源,断开连接。
4.2 TCP网络程序
服务端实现:
public class ServerTCP {
public static void main(String[] args) throws IOException {
System.out.println("服务端启动 , 等待连接 .... ");
// 1.创建 ServerSocket对象,绑定端口,开始等待连接
ServerSocket ss = new ServerSocket(8888);
// 2.接收连接 accept 方法, 返回 socket 对象.
Socket server = ss.accept();
// 3.通过socket 获取输入流
InputStream is = server.getInputStream();
// 4.一次性读取数据
// 4.1 创建字节数组
byte[] b = new byte[1024];
// 4.2 据读取到字节数组中.
int len = is.read(b);
// 4.3 解析数组,打印字符串信息
String msg = new String(b, 0, len);
System.out.println(msg);
// =================回写数据=======================
// 5. 通过 socket 获取输出流
OutputStream out = server.getOutputStream();
// 6. 回写数据
out.write("我很好,谢谢你".getBytes());
// 7.关闭资源.
out.close();
is.close();
server.close();
}
}
客户端实现:
public class ClientTCP {
public static void main(String[] args) throws Exception {
System.out.println("客户端 发送数据");
// 1.创建 Socket ( ip , port ) , 确定连接到哪里.
Socket client = new Socket("localhost", 8888);
// 2.通过Scoket,获取输出流对象
OutputStream os = client.getOutputStream();
// 3.写出数据.
os.write("你好么? tcp ,我来了".getBytes());
// ==============解析回写=========================
// 4. 通过Scoket,获取 输入流对象
InputStream in = client.getInputStream();
// 5. 读取数据数据
byte[] b = new byte[100];
int len = in.read(b);
System.out.println(new String(b, 0, len));
// 6. 关闭资源 .
in.close();
os.close();
client.close();
}
}
结果如图
五、练习–上传文件
5.1 C/S基础版本–单线程
-
服务器端实现
读取客户端上传的文件,保存到服务器的硬盘,给客户端回写"上传成功"
public class TCPServer { public static void main(String[] args) throws IOException { //1.创建一个服务器ServerSocket对象,和系统要指定的端口号 ServerSocket server = new ServerSocket(8888); //2.使用ServerSocket对象中的方法accept,获取到请求的客户端Socket对象 Socket socket = server.accept(); //3.使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象 InputStream is = socket.getInputStream(); //4.判断d:\\upload文件夹是否存在,不存在则创建 File file = new File("D:\\upload"); if(!file.exists()){ file.mkdirs(); } //5.创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地 FileOutputStream fos = new FileOutputStream(file+"\\1.jpg"); //6.使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件 System.out.println("11111111111111111111"); int len =0; byte[] bytes = new byte[1024]; while((len = is.read(bytes))!=-1){ //7.使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件保存到服务器的硬盘上 fos.write(bytes,0,len); } System.out.println("22222222222222222222222 while死循环打印不到"); //8.使用Socket对象中的方法getOutputStream,获取到网络字节输出流OutputStream对象 //9.使用网络字节输出流OutputStream对象中的方法write,给客户端回写"上传成功" socket.getOutputStream().write("上传成功".getBytes()); //10.释放资源(FileOutputStream,Socket,ServerSocket) fos.close(); socket.close(); server.close(); } }
-
客户端实现
读取本地文件,上传到服务器,读取服务器回写的数据
public class TCPClient { public static void main(String[] args) throws IOException { //1.创建一个本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源 FileInputStream fis = new FileInputStream("D:\\1.jpg"); //2.创建一个客户端Socket对象,构造方法中绑定服务器的IP地址和端口号 Socket socket = new Socket("127.0.0.1",8888); //3.使用Socket中的方法getOutputStream,获取网络字节输出流OutputStream对象 OutputStream os = socket.getOutputStream(); //4.使用本地字节输入流FileInputStream对象中的方法read,读取本地文件 int len = 0; byte[] bytes = new byte[1024]; while((len = fis.read(bytes))!=-1){ //5.使用网络字节输出流OutputStream对象中的方法write,把读取到的文件上传到服务器 os.write(bytes,0,len); } /* 解决:上传完文件,给服务器写一个结束标记 void shutdownOutput() 禁用此套接字的输出流。 对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。 */ socket.shutdownOutput(); //6.使用Socket中的方法getInputStream,获取网络字节输入流InputStream对象 InputStream is = socket.getInputStream(); System.out.println("333333333333333333333"); //7.使用网络字节输入流InputStream对象中的方法read读取服务回写的数据 while((len = is.read(bytes))!=-1){ System.out.println(new String(bytes,0,len)); } System.out.println("444444444444444444 while死循环打印不到"); //8.释放资源(FileInputStream,Socket) fis.close(); socket.close(); } }
5.2 C/S 升级版本–多线程
客户端实现不变,服务器实现如下:
public class TCPServer {
public static void main(String[] args) throws IOException {
//1.创建一个服务器ServerSocket对象,和系统要指定的端口号
ServerSocket server = new ServerSocket(8888);
//2.使用ServerSocket对象中的方法accept,获取到请求的客户端Socket对象
/* 让服务器一直处于监听状态(死循环accept方法) 有一个客户端上传文件,就保存一个文件 */
while(true){
Socket socket = server.accept();
/* 使用多线程技术,提高程序的效率 有一个客户端上传文件,就开启一个线程,完成文件的上传 */
new Thread(new Runnable() {
//完成文件的上传
@Override
public void run() {
try {
//3.使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//4.判断d:\\upload文件夹是否存在,不存在则创建
File file = new File("d:\\upload");
if(!file.exists()){
file.mkdirs();
}
/* 自定义一个文件的命名规则:防止同名的文件被覆盖 规则:毫秒值+随机数 */
String fileName = System.currentTimeMillis()+new Random().nextInt(999999)+".jpg";
//5.创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地
//FileOutputStream fos = new FileOutputStream(file+"\\1.jpg");
FileOutputStream fos = new FileOutputStream(file+"\\"+fileName);
//6.使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件
int len =0;
byte[] bytes = new byte[1024];
while((len = is.read(bytes))!=-1){
//7.使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件保存到服务器的硬盘上
fos.write(bytes,0,len);
}
//8.使用Socket对象中的方法getOutputStream,获取到网络字节输出流OutputStream对象
//9.使用网络字节输出流OutputStream对象中的方法write,给客户端回写"上传成功"
socket.getOutputStream().write("上传成功".getBytes());
//10.释放资源(FileOutputStream,Socket,ServerSocket)
fos.close();
socket.close();
}catch (IOException e){
System.out.println(e);
}
}
}).start();
}
//服务器就不用关闭
//server.close();
}
}
5.3 B/S 升级版本
客户端浏览器,web代码就不粘贴了,详情已放到下载区–java进阶代码11-Net
服务器实现
public class TCPServer {
public static void main(String[] args) throws IOException {
//创建一个服务器ServerSocket,和系统要指定的端口号
ServerSocket server = new ServerSocket(8080);
//使用accept方法获取到请求的客户端对象(浏览器)
Socket socket = server.accept();
//使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//使用网络字节输入流InputStream对象中的方法read读取客户端的请求信息
/*byte[] bytes = new byte[1024]; int len = 0; while((len = is.read(bytes))!=-1){ System.out.println(new String(bytes,0,len)); }*/
//把is网络字节输入流对象,转换为字符缓冲输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//把客户端请求信息的第一行读取出来 GET /11_Net/web/index.html HTTP/1.1
String line = br.readLine();
//把读取的信息进行切割,只要中间部分 /11_Net/web/index.html
String[] arr = line.split(" ");
//把路径前边的/去掉,进行截取 11_Net/web/index.html
String htmlpath = arr[1].substring(1);
//创建一个本地字节输入流,构造方法中绑定要读取的html路径
FileInputStream fis = new FileInputStream(htmlpath);
//使用Socket中的方法getOutputStream获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 写入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());
//一读一写复制文件,把服务读取的html文件回写到客户端
int len = 0;
byte[] bytes = new byte[1024];
while((len = fis.read(bytes))!=-1){
os.write(bytes,0,len);
}
//释放资源
fis.close();
socket.close();
server.close();
}
}
图解
但是运行客户端浏览器发现图片不显示,经下面修改后,图片显示正常
public class TCPServerThread {
public static void main(String[] args) throws IOException {
//创建一个服务器ServerSocket,和系统要指定的端口号
ServerSocket server = new ServerSocket(8080);
/* 浏览器解析服务器回写的html页面,页面中如果有图片,那么浏览器就会单独的开启一个线程,读取服务器的图片 我们就的让服务器一直处于监听状态,客户端请求一次,服务器就回写一次 */
while(true){
//使用accept方法获取到请求的客户端对象(浏览器)
Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
//使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//使用网络字节输入流InputStream对象中的方法read读取客户端的请求信息
/*byte[] bytes = new byte[1024]; int len = 0; while((len = is.read(bytes))!=-1){ System.out.println(new String(bytes,0,len)); }*/
//把is网络字节输入流对象,转换为字符缓冲输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//把客户端请求信息的第一行读取出来 GET /11_Net/web/index.html HTTP/1.1
String line = br.readLine();
System.out.println(line);
//把读取的信息进行切割,只要中间部分 /11_Net/web/index.html
String[] arr = line.split(" ");
//把路径前边的/去掉,进行截取 11_Net/web/index.html
String htmlpath = arr[1].substring(1);
//创建一个本地字节输入流,构造方法中绑定要读取的html路径
FileInputStream fis = new FileInputStream(htmlpath);
//使用Socket中的方法getOutputStream获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 写入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());
//一读一写复制文件,把服务读取的html文件回写到客户端
int len = 0;
byte[] bytes = new byte[1024];
while((len = fis.read(bytes))!=-1){
os.write(bytes,0,len);
}
//释放资源
fis.close();
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}).start();
}
//server.close();
}
}