JAVASE——4.网络编程
Socket 编程
套接字使用TCP提供了两台计算机之间的通信机制。 客户端程序创建一个套接字,并尝试连接服务器的套接字。
当连接建立时,服务器会创建一个 Socket 对象。客户端和服务器现在可以通过对 Socket 对象的写入和读取来进行通信。
java.net.Socket 类代表一个套接字,并且 java.net.ServerSocket 类为服务器程序提供了一种来监听客户端,并与他们建立连接的机制。
以下步骤在两台计算机之间使用套接字建立TCP连接时会出现:
- 服务器实例化一个 ServerSocket 对象,表示通过服务器上的端口通信。
- 服务器调用 ServerSocket 类的 accept() 方法,该方法将一直等待,直到客户端连接到服务器上给定的端口。
- 服务器正在等待时,一个客户端实例化一个 Socket 对象,指定服务器名称和端口号来请求连接。
- Socket 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够与服务器进行通信。
- 在服务器端,accept() 方法返回服务器上一个新的 socket 引用,该 socket 连接到客户端的 socket。
连接建立后,通过使用 I/O 流在进行通信,每一个socket都有一个输出流和一个输入流,客户端的输出流连接到服务器端的输入流,而客户端的输入流连接到服务器端的输出流。
TCP 是一个双向的通信协议,因此数据可以通过两个数据流在同一时间发送.以下是一些类提供的一套完整的有用的方法来实现 socket。
package socket;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 聊天室客户端
*/
public class Client {
/**
* java.net.Socket 套接字
* Socket封装了TCP协议的通讯细节,我们使用他可以与远端计算机建立TCP链接。并
* 基于一堆流的IO操作完成与远端计算机的数据交换。
*/
private Socket socket;
/**
* 初始化客户端
*/
public Client(){
try {
/*
实例化Socket时需要传入两个参数:
参数1:远端计算机的地址信息
参数2:远端计算机打开的服务端口
上述构造器实例化的过程就是与远端计算机建立连接的过程,如果成功建立连接
则实例化成功,否则构造器会抛出异常。
我们通过远端计算机地址(IP)可以找到网络上的远端计算机,通过服务端口可以链接上运行
在该计算机上的服务端应用程序(就是这个程序开打的该端口等待我们
链接的。)
本机地址信息可以选取:
localhost
127.0.0.1
*/
System.out.println("正在连接服务端");
socket = new Socket("172.18.10.93",8090);
System.out.println("与服务端建立连接");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start(){
try {
/*
通过Socket的方法:
OutputStream getOutputStream()
获取的字节输出流写出的字节会通过网络发送给远端建立好链接的计算机
*/
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw,true);
Scanner scanner = new Scanner(System.in);
while(true){
String line = scanner.nextLine();
if("exit".equalsIgnoreCase(line)){
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
socket.close();//与远端计算机断开连接,进行TCP挥手
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
Client client = new Client();
client.start();
}
}
package socket;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 聊天室服务端
*/
public class Server {
/**
* java.net.ServerSocket
* ServerSocket是运行在服务端的,他的主要工作:
* 1:打开服务端口(客户端就是通过这个端口与服务器建立连接)
* 2:监听该服务端口,一旦一个客户端链接,则会返回一个Socket实例,并通过这个
* Socket实例与链接的客户端进行交互
*
* 如果我们将Socket比喻为“电话”,那么ServerSocket相当于“总机”。
*/
private ServerSocket server;
public Server(){
try {
/*
实例化ServerSocket的同时需要指定打开的服务端口,客户端就是通过该端口建立链接的。
如果该端口已经被操作系统其他程序占用了,那么这里会抛出异常:
java.net.BindException:address already in use
绑定异常 地址, 已经 被占用了
*/
System.out.println("正在启动服务端。。。");
server = new ServerSocket(8888);
System.out.println("服务端启动完毕");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
/*
ServerSocket提供的方法:
Socket accept()
该方法是一个阻塞方法,调用后开始等待,直到一个客户端与服务端建立链接为止,
此时该方法会立即返回一个Socket,通过Socket与该客户端交互
*/
while(true){
System.out.println("等待客户端链接。。。");
Socket socket = server.accept();
System.out.println("一个客户端链接了!");
/*
通过Socket的方法:
InputStream getInputStream()
获取一个字节输入流,就可以读取来自远端计算机发送过来的字节数据
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
String line ;
/*
服务端在读取客户端消息这里,如果客户端没有调用socket.close()与服务端
正常断开连接(例如客户端直接被杀掉了进程等操作),那么服务端这里会抛出
一个异常:SocketException: Connection reset
这是由于客户端非正常操作导致的,服务端无法通过逻辑避免该异常的产生。
*/
while((line = br.readLine())!=null){
System.out.println("客户端说"+line);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
需要注意的几个点:
1:当客户端不再与服务端通讯时,需要调用socket.close()断开链接,此时会发送断开链接的信号给服务端。这时服务端的br.readLine()方法会返回null,表示客户端断开了链接。
2:当客户端链接后不输入信息发送给服务端时,服务端的br.readLine()方法是出于阻塞状态的,直到读取了一行来自客户端发送的字符串。
多客户端链接
之前只有第一个连接的客户端可以与服务端说话。
原因:
服务端只调用过一次accept方法,因此只有第一个客户端链接时服务端接受了链接并返回了Socket,此时可以与其交互。
而第二个客户端建立链接时,由于服务端没有再次调用accept,因此无法与其交互。
package socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 聊天室服务端
*/
public class Server {
/**
* 运行在服务端的ServerSocket主要完成两个工作:
* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接
* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket
* 就可以和该客户端交互了
*
* 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个
* 电话使得服务端与你沟通。
*/
private ServerSocket serverSocket;
/**
* 服务端构造方法,用来初始化
*/
public Server(){
try {
System.out.println("正在启动服务端...");
/*
实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他
应用程序占用的端口相同,否则会抛出异常:
java.net.BindException:address already in use
端口是一个数字,取值范围:0-65535之间。
6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
while(true) {
System.out.println("等待客户端链接...");
/*
ServerSocket提供了接受客户端链接的方法:
Socket accept()
这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端
的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例
通过这个Socket就可以与客户端进行交互了。
可以理解为此操作是接电话,电话没响时就一直等。
*/
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
/*
Socket提供的方法:
InputStream getInputStream()
获取的字节输入流读取的是对方计算机发送过来的字节
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String message = null;
while ((message = br.readLine()) != null) {
System.out.println("客户端说:" + message);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
添加循环操作后,发现依然无法实现。(要用到线程的内容)
原因在于:
外层的while循环里面嵌套了一个内层循环(循环读取客户端发送消息),而循环执行机制决定了里层循环不结束,外层循环则无法进入第二次操作。
printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}