今天学习的内容是TCP传输
TCP传输主要使用Socket类、ServerSocket 类和IO流类。其中Socket类表示要进行连接的套接字,ServerSocket类表示服务器端用于监听客户端Socket连接请求的套接字,而网络数据传输则要借助相关的IO流来完成。TCP传输的基本模式:
- 服务器端:
- 使用ServerSocket(port)创建用于监听客户端发来的Socket连接请求的ServerSocket对象,并绑定到指定端口(此端口标识服务器端进程)
- 如果收到请求,ServerSocket类的accept()方法会返回一个与客户端Socket对象相连接的Socket对象,注意此对象还是绑定到标识服务器端进程的端口!如果没有收到请求,accept()方法阻塞
- 使用Socket类的getInputStream()和getOutputStream()方法返回的IO流读写数据
- 客户端:
- 使用Socket(inetAddress,port)创建客户端Socket对象(系统会指定任意可用端口来标识客户端进程,并将此对象绑定到此端口),并向指定IP地址对象与端口号的ServerSocket对象发出Socket连接请求
- 使用socket类的getInputStream()和getOutputStream()方法返回的IO流读写数据
注意:套接字必须先进行连接才能传输数据,这就是TCP传输的特点!
示例程序(结合多线程与GUI技术的聊天室程序,还添加了文本文件上传功能):
public class Server {
private List<PrintWriter> outList;// 存储Socket的字节输出流的列表,用于将某用户发送过来的内容广播给所有用户
public static void main(String[] args) {
new Server().waitAndConnect();
}
// 主线程的任务:等待客户端的Socket连接请求,收到请求后,ServerSocket类的accept()方法会返回一个与客户端Socket对象相连接的Socket对象,然后继续等待
private void waitAndConnect() {
outList = new ArrayList<>();
try {
@SuppressWarnings("resource")
ServerSocket serverSocket = new ServerSocket(8888);// 创建用于监听客户端发来的Socket连接请求的ServerSocket对象,并与8888端口绑定(8888端口标识服务器端进程)
while (true) {
Socket socket = serverSocket.accept();// 如果收到请求,返回一个与客户端Socket对象相连接的Socket对象,注意这个Socket对象还是与8888端口绑定!!
Thread t = new Thread(new ReadAndBroadcast(socket));
t.start();
System.out.println("完成本地" + socket.getLocalAddress().getHostAddress() + "--" + socket.getLocalPort()
+ "与用户" + socket.getInetAddress().getHostAddress() + "--" + socket.getPort() + "的连接");
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 服务器端新开线程的任务:读取某用户发送过来的内容,然后广播给所有用户
public class ReadAndBroadcast implements Runnable {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private PrintWriter pw;
public ReadAndBroadcast() {
}
public ReadAndBroadcast(Socket socket) {
this.socket = socket;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));// 将Socket返回的字节输入流转换为字符输入流并用缓冲区装饰
out = new PrintWriter(this.socket.getOutputStream());// 将Socket返回的字节输出流转换为字符输出流PrintWriter
outList.add(out);// 将PrintWriter存储进列表中
pw = new PrintWriter("test_receive.txt");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
String line = null;
try {
while ((line = in.readLine()) != null) {// 读取某用户发送过来的内容,如果客户端socket关闭,那么再使用服务器端socket的IO流就会发生异常!!!
System.out.println("读取到用户" + socket.getInetAddress().getHostAddress() + "--" + socket.getPort()
+ "的信息:" + line);
pw.println(line);// 将读取的信息写入文件
pw.flush();
Iterator<PrintWriter> it = outList.iterator();
while (it.hasNext()) {// 广播给所有用户
PrintWriter out = it.next();
out.println(line);
out.flush();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
pw.close();
socket.close();// 服务器端Socket要在客户端与之连接的Socket对象关闭后,调用其close()方法进行显式关闭
System.out.println("socket close success");
} catch (IOException e) {
System.out.println("关闭资源失败");
}
}
}
}
}
public class Client {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private BufferedReader br;
private JTextField outgoing;
private JTextArea incoming;
public static void main(String[] args) {
new Client().gui();
}
// GUI设置
private void gui() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(700, 400);
frame.setVisible(true);
frame.setLocationRelativeTo(null);
JLabel label = new JLabel("聊天室");
label.setFont(new Font("serif", Font.BOLD, 20));
frame.getContentPane().add(BorderLayout.NORTH, label);
outgoing = new JTextField(20);
incoming = new JTextArea(15, 50);
incoming.setLineWrap(true);
incoming.setWrapStyleWord(true);
incoming.setEditable(false);
JPanel mainpanel = new JPanel();
JScrollPane panel = new JScrollPane(incoming);
panel.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
panel.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
mainpanel.add(panel);
mainpanel.add(outgoing);
frame.getContentPane().add(BorderLayout.CENTER, mainpanel);
JButton button_send = new JButton("send");
JButton button_upload = new JButton("upload");
mainpanel.add(button_send);
mainpanel.add(button_upload);
button_send.addActionListener(new ActionListener() {// 主线程任务:用户按下发送按钮即将用户输入内容写入服务器
public void actionPerformed(ActionEvent e) {
try {
out.println("用户" + socket.getLocalAddress().getHostAddress() + "--" + socket.getLocalPort() + " :"
+ outgoing.getText());
out.flush();
outgoing.setText(" ");
outgoing.requestFocus();
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
button_upload.addActionListener(new ActionListener() {// 主线程任务:用户按下上传按钮即将指定文件内容写入服务器
public void actionPerformed(ActionEvent e) {
try {
String line = null;
while((line = br.readLine())!= null){
out.println(line);
System.out.println(line);
out.flush();
}
} catch (Exception ex) {
ex.printStackTrace();
} finally{
if(br!=null){
try {
br.close();
System.out.println("br closed");
} catch (IOException e1) {
System.out.println("关闭资源失败");
}
}
}
}
});
setUpNet();
Thread t = new Thread(new readFromServer());
t.start();
}
// Socket设置
private void setUpNet() {
try {
socket = new Socket(InetAddress.getLocalHost(), 8888);// 创建客户端Socket对象(系统会指定任意可用端口来标识客户端进程,并将此对象绑定到此端口),并向服务器端监听8888端口的ServerSocket对象发出Socket连接请求
out = new PrintWriter(socket.getOutputStream());// 将Socket返回的字节输出流转换为字符输出流PrintWriter
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));// 将Socket返回的字节输入流转换为字符输入流并用缓冲区装饰
br = new BufferedReader(new FileReader("test.txt"));// 用于读取文本文件的输入流
} catch (IOException e) {
e.printStackTrace();
}
}
// 新开线程任务:读取服务器广播的数据并显示在文本域中
public class readFromServer implements Runnable {
@Override
public void run() {
String line = null;
try {
while ((line = in.readLine()) != null) {
incoming.append(line + "\n");
}
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
in.close();
System.out.println("in closed");// 本语句不会执行
} catch (IOException e) {
System.out.println("关闭资源失败");
}
}
}
}
}
聊天室TCP传输示意图:
关于TCP传输,我一直有两个疑问:
- 客户端与服务器端的Socket对象绑定的到底是那个端口?
- Socket对象、ServerSocket对象和Socket对象返回的IO流该如何关闭?
之前我以为ServerSocket类的accept()方法返回的Socket对象绑定的是不同于ServerSocket对象的端口号,但是一个进程又只能由一个端口标识,这就产生了矛盾。实际上,客户端Socket对象绑定的是标识客户端进程的端口,而服务器端的Socket对象与ServerSocket对象一样,都绑定了服务器端进程的端口!也就是说,一个端口只能标识一个进程,却能绑定多个Socket!
关于第二个问题我总结了几个原则:
- Socket对象和ServerSocket对象都可以使用close()方法进行显式关闭
- Socket对象关闭,其返回的IO流也会关闭
- Socket对象关闭,与之关联的通道也会关闭
- Socket对象关闭,与之连接的Socket对象不会关闭
- 如果客户端Socket对象关闭,再使用服务器端Socket对象的IO流进行读写操作,会发生SocketException(读取操作Connection reset;写入操作Connect reset by peer)
- 客户端Socket对象一般不使用close()方法关闭,而是当用户主动关闭客户端进程或者由异常引起客户端进程关闭时,自动关闭Socket对象
- 服务器端Socket要在客户端与之连接的Socket对象关闭后,调用其close()方法进行显式关闭
- ServerSocket对象一般不需要关闭