TCP通信-Java

1. 概述

通信步骤

​ 服务器端先启动,服务器端不会主动的请求客户端,必须使用客户端请求服务器端。客户端和服务器端会建立一个逻辑链接,而这个连接中包含一个IO对象,客户端使用这个IO对象进行通信。通信的数据不只是字符,所以是字节流对象。

服务器端必须明确两件事

  1. 多个客户端同时和服务器端进行交互,服务器端必须明确和哪个客户端进行的交互。服务器端有个方法,叫accept客户端获取到请求的客户端对象。

  2. 多个客户端同时和服务器进行交互,就需要使用多个IO流对象。

  3. 服务器端是没有IO流的,服务器可以获取到请求的客户流对象Socket,使用每个客户端Socket中提供的IO流和客户端进行交互。

    • 服务器端使用客户端的字节输入流读取客户端发送数据

    • 服务器端使用客户端的字节输出流给客户端回写数据

在Java中提供了两个类实现TCP通信程序

  1. 客户端:java.net.Socket类表示,创建Socket对象,向服务端发出连接请求,服务器端响应请求,两者建立连接开始通信
  2. 服务端:java.net.SeverSocket类表示。创建SeverSocket对象,先当与开启一个服务,并等待客户端的连接

2. Socket类

Socket类:该类实现客户端套接字(两台设备之间通讯的端点)

  • public Socket(String host,int port):创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null,则相当于指定地址为回送地址。
  • OutputStream getOutputStream():返回此套接字的输出流
  • InputStream getInputStream:返回此套接字的输入流
  • void close():关闭此套接字

3. SeverSocket类

  • SeverSocket(int port):创建绑定到端口的服务器套接字
  • Socket accept():侦听并接收此套接字的连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//客户端代码
public class TCPClient {
public static void main(String[] args) throws IOException {
//创建客户端对象中的方法Socket,构造方法绑定服务器的IP地址和端口号
Socket socket = new Socket("127.0.0.1", 8888);
//使用Socket对象中的getOutputStream()获取网络字节输出流对象
OutputStream os = socket.getOutputStream();
//使用网络字节输出流对象中的write方法,给服务器发送数据
os.write("你好,服务器".getBytes());
//使用Socket对象中的getInputStream方法获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//使用网络字节输入流对象中的方法read,读取服务器回写的数据
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes, 0, len));
socket.close();
}
}
//服务器代码
public class TCPSever {
public static void main(String[] args) throws IOException {
//创建服务器SeverSocket对象和系统要指定的端口号
ServerSocket server = new ServerSocket(8888);
//使用SeverSocket对象中的方法accept,获取到请求的客户端对象Socket
Socket socket = server.accept();
//使用Socket对象中的getInputStream方法获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//使用网络字节输入流对象中的方法read,读取客户端发送来的数据
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes, 0, len));
//使用Socket对象中的getOutputStream()获取网络字节输出流对象
OutputStream os = socket.getOutputStream();
//使用网络字节输出流对象中的write方法,给客户端发送数据
os.write("收到谢谢".getBytes());
socket.close();
server.close();
}
}

文件上传案例

  • 当客户端向服务器发送数据时,不会将结束标记发送过去,服务器将会处于循环等待,所以要使用shutdownOutput方法。

    void shutdownOutput():禁用此套接字的输出流。对于TCP套接字,任何以前写入的数据都将被发送,并且后跟TCP的正常连接终止序列

  • 为防止同名文件被覆盖,自定义一个文件的命名规则:域名+毫秒值+随机数

  • 服务器要一直处于监听状态(死循环accept方法)。使用多线程技术,提高程序的效率,有一个客户端上传文件就开启一个线程,完成文件上传

服务器端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class TCPSever {
public static void main(String[] args) throws IOException {
//创建服务器SeverSocket对象和系统要指定的端口号
ServerSocket server = new ServerSocket(8888);
while (true){
//使用SeverSocket对象中的方法accept,获取到请求的客户端对象Socket
Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try{
File file = new File("D:\\Blog\\images\\JAVA");
if(!file.exists()){
file.mkdirs();
}
String filename = "hxx"+System.currentTimeMillis()+ new Random(9999).nextInt();
//创建一个本地输出流
FileOutputStream fos = new FileOutputStream(file+"\\"+filename+".png");

//使用Socket对象中的getInputStream方法获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();

//使用网络字节输入流对象中的方法read,读取客户端发送来的数据
byte[] bytes = new byte[1024];
int len;
while ((len = is.read(bytes))!=-1){
fos.write(bytes,0,len);//写到硬盘
}
//使用Socket对象中的getOutputStream()获取网络字节输出流对象
//使用网络字节输出流对象中的write方法,给客户端发送数据
OutputStream os = socket.getOutputStream();
os.write("上传成功".getBytes());
socket.shutdownOutput();
}catch (IOException e){
System.out.println(e);
}
}
}).start();
}
//服务器不停机
//server.close();
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TCPClient {
public static void main(String[] args) throws IOException {
//读取本地文件,创建本地输入流
FileInputStream fis = new FileInputStream("xin.png");
//创建客户端对象中的方法Socket,构造方法绑定服务器的IP地址和端口号
Socket socket = new Socket("127.0.0.1", 8888);
//使用Socket对象中的getOutputStream()获取网络字节输出流对象
OutputStream os = socket.getOutputStream();

byte[] bytes = new byte[1024];
int len = 0;
//使用本地字节输入流FileInputStream对象中的方法read读取本地文件
while ((len = fis.read(bytes))!= -1){
//使用网络字节输出流对象中的write方法,给服务器发送数据
os.write(bytes,0,len);
}
socket.shutdownOutput();

//使用Socket对象中的getInputStream方法获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//使用网络字节输入流对象中的方法read,读取服务器回写的数据
len = 0;
while ((len = is.read(bytes))!= -1){
System.out.println(new String(bytes,0,len));
}
socket.close();
fis.close();
}
}

模拟BS服务器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//http://127.0.0.1:8080/day01_code/web/index.html
public class BSserver {
public static void main(String[] args) throws IOException {
//创建服务器SeeverSocket类,
ServerSocket server = new ServerSocket(8080);
//浏览器解析服务器回写的页面,如果有图片那么浏览器会单独开一个线程,所以服务器要一直监听
while(true){
Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
//向浏览器端回写页面数据
//把is网络字节输入流对象,转换为字符缓冲输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//把请求信息的第一行读取出来
String s = br.readLine();
//切割信息
String[] s1 = s.split(" ");
String htmlPath = s1[1].substring(1);//day01_code/web/index.html

//创建本地字节输入流,构造方法中绑定要读取的HTML文件路径
FileInputStream fis = new FileInputStream(htmlPath);
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文件回写到客户端
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
fis.close();
socket.close();
}catch (IOException e){
System.out.println(e);
}
}
}).start();
}
}
}