多线程Socket通信
对于服务端来说,可能有多个客户端连接进来,从平衡上考虑就应当为每个客户端的连接单独开启一个线程,并且在main进程中继续用accept()等待其它用户连接。
对于客户端来说,虽然不涉及多个服务端问题,但是数据输入流的readXxx()方法会引起阻塞,所以可以单独放到子线程中,不影响主线程的执行。
书上的例子,改了改,这个例子有挺多细节值得考虑的。
服务端ThreadServer.java
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class ThreadServer {
public static void main(String[] args) {
ServerSocket ss;
Socket sck;
while (true) {
try {
ss = new ServerSocket(3838);
System.out.println("等待客户端连接...");
sck = ss.accept();// 阻塞以等待连接
System.out.println("[+]客户端地址:" + sck.getInetAddress());
if (sck != null) {
// 每个客户启动一个专门的线程
new MyThread(sck).start();
}
} catch (IOException e) {
// 不能重复创建ServerSocket
}
}
}
}
// 线程类,每个客户单独一个线程,继承Thread类方便一些
class MyThread extends Thread {
Socket sck;// 服务器端Socket
DataOutputStream dos;// 数据输出流
DataInputStream dis;// 数据输入流
String s;
// 构造器中获取这些成员
public MyThread(Socket sck) {
this.sck = sck;
try {
dos = new DataOutputStream(this.sck.getOutputStream());
dis = new DataInputStream(this.sck.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
double r;
while (true) {
try {
r = dis.readDouble();// 从输入流中读取信息,即客户端发送来的半径
dos.writeDouble(Math.PI * r * r);// 从输出流发送给客户端
} catch (IOException e) {
System.out.println("[-]客户端" + sck.getInetAddress() + "断开连接");
return;// 结束这个线程
}
}
}
}
}
客户端ThreadClient.java
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class ThreadClient {
public static void main(String[] args) {
Socket sck;
DataInputStream dis;// 输入流
DataOutputStream dos = null;// 输出流
Thread thd;// 线程对象
MyThread mythd;// 目标对象
InetAddress ia;// 地址对象
InetSocketAddress isa;// 套接字地址对象,用来为Socket建立连接
try {
sck = new Socket();// 无参构造的Socket对象,在后面再建立连接
mythd = new MyThread();// 目标对象
thd = new Thread(mythd);// 建立线程对象
ia = InetAddress.getByName("127.0.0.1");// 对这个地址对象实例化
isa = new InetSocketAddress(ia, 3838);// 套接字地址对象,多提供一个端口
sck.connect(isa);// 为Socket对象建立连接
dis = new DataInputStream(sck.getInputStream());// 输入流
dos = new DataOutputStream(sck.getOutputStream());// 输出流
mythd.setDataInputStream(dis);// 将输入流对象传入给子线程目标对象
// 即便线程对象的构造方法在这前面,也会影响到线程对象
thd.start();// 启动子线程去做输入流操作,因为输入流可能阻塞
} catch (UnknownHostException e) {
System.out.println("[x]无法解析地址");
System.exit(0);
} catch (IOException e) {
System.out.println("[x]无法连接到服务端");
System.exit(0);
}
// 子线程阻塞与否不影响主线程,主线程中做输出流操作
System.out.println(">输入圆的半径:");
Scanner sc = new Scanner(System.in);
while (sc.hasNext())// main线程的阻塞之处
{
double r;
try {
r = sc.nextDouble();// 读入圆的半径
dos.writeDouble(r);// 从输出流发送给服务端
} catch (Exception e) {
System.out.println("[x]输入错误,程序结束");
System.exit(0);
}
}
}
}
// 线程目标对象
class MyThread implements Runnable {
DataInputStream dis;// 输入流
// 将输入流对象的引用给子线程,在子线程里做输入,防止阻塞main线程
public void setDataInputStream(DataInputStream dis) {
this.dis = dis;
}
@Override
public void run() {
double result;
while (true) {
try {
// 从输入流中读取计算结果,即服务器的输出流
result = this.dis.readDouble();// 子线程的阻塞之处
System.out.println("[+]服务器计算得:" + result);
// 放在这里而不是主线程中,才能保证提示输入在计算结果之后
System.out.println(">输入圆的半径:");
} catch (IOException e) {
System.out.println("[-]与服务器断开");
System.exit(0);
}
}
}
}
输出(正常)
>输入圆的半径:
2.3
[+]服务器计算得:16.619025137490002
>输入圆的半径:
3.1
[+]服务器计算得:30.190705400997913
>输入圆的半径:
bye
[x]输入错误,程序结束
等待客户端连接...
[+]客户端地址:/127.0.0.1
等待客户端连接...
[-]客户端/127.0.0.1断开连接
输出(客户端直接退出)
>输入圆的半径:
2
[+]服务器计算得:12.566370614359172
>输入圆的半径:
等待客户端连接...
[+]客户端地址:/127.0.0.1
等待客户端连接...
[-]客户端/127.0.0.1断开连接
输出(服务端宕机)
>输入圆的半径:
1.1
[+]服务器计算得:3.8013271108436504
>输入圆的半径:
[-]与服务器断开
等待客户端连接...
[+]客户端地址:/127.0.0.1
等待客户端连接...