TCP协议提供可靠的数据传输服务是通过建立TCP连接实现的。一条“TCP连接”连接的两端是Internet上分别在两台主机运行的两个进程,一个是发送进程,一个是接收进程,每个进程用一个Socket(IP地址和端口)唯一确定。一对Socket唯一标识一条TCP连接。TCP连接是全双工和点对点的,全双工指数据可双向传输,点对点是指每条TCP连接只有两个端点。
使用一对socket进行tcp连接,连接成功后,就可以通过socket发送或接收字节流数据,完成点到点、全双工的通信。
服务端通过创建指定端口号的 ServerSocket
对象,调用accept()方法等待客户端连接请求,等待期间当前进程处于堵塞状态。当服务端接收到客户端的连接请求时,进程继续运行,并建立一条TCP连接,accept()完成并返回一个Socket对象,通过该Socket对象与客户端的Socket对象实现实时的数据通信。
ServerSocket serverSocket = new ServerSocket(7000); Socket socket = serverSocket.accept();
客户端创建Socket对象,指定服务器主机的IP和端口,发出TCP连接请求,待服务器接受连接请求后Socket对象创建成功,此时可以通过此Socket对象与服务端Socket对象实时通信。
Socket socket = new Socket("127.0.0.1", 7000);
从控制台获取输入的字符串,字符串数据将从PrintWriter"流入"OutputStreamWriter在"流入"socket的OutputStream中,通过TCP建立的链路,流到与之对应的Socket的InputStream中。注意,Socket总是成对的。
PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream())); message = scanner.nextLine(); writer.println(message); writer.flush();
在另一端的Socket中,便可以通过InputStream把发送的数据读取出来,此时数据的流向为InputStream->InputStreamReader->BufferedReader->控制台。
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String line = ""; while ((line = bufferedReader.readLine()) != null) { System.out.println(line); }
整个的数据流向为:客户端控制台->PrintWriter->OutputStreamWriter->客户端Socket的OutputStream->TCP链路->服务端Socekt的InputStream->InputStreamReader->BufferedReader->服务端控制台。通过socket便可以轻易的将数据从客户端发送到服务端了。
了解了Socekt通信后,我们可以使用Socket仿照QQ群聊效果:当某个用户发送消息时,所有用户都接收到此消息并显示到控制台上。
客户端需要两个进程,一个进程不停的从控制台中读取数据,读取到用户输入时即向服务端发送读取到的数据,把它称为写进程。另一个进程接收服务端发送的数据,并把它显示到控制台上,称为读进程。
public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); Socket socket = new Socket("127.0.0.1", 7000); // 获取输入流和输出流 PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream())); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); // 开启两个线程分别读写 // 写线程 new Thread(() -> { // 循环从控制台读取数据 String message = ""; while (!message.equals("exit")) { message = scanner.nextLine(); writer.println(message); writer.flush(); } writer.close(); }).start(); // 读线程 new Thread(() -> { // 不停从input流获取数据 String line = ""; try { while ((line = bufferedReader.readLine()) != null) { System.out.format("%50s", line); System.out.println(); } bufferedReader.close(); } catch (IOException e) { } }).start(); }
为了响应多个客户端连接,需要在循环中不停的调用accept()方法,每当获取到一个新TCP连接时,把获取到的Socket对象存入set中,对每个Socket对象都开启一个线程,主要的任务是不停的从InputStream中读取数据,即接收发送客户端数据。获取到数据后,再发送给所有已连接的客户端Socket(除了发送数据的客户端Socket),即遍历set发送数据。
public class Server { public static int userCount = 0; public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(7000); Set<Socket> socketSet = new HashSet<>(); while (true) { Socket socket = serverSocket.accept(); socketSet.add(socket); userCount++; // 开启一个新线程 Thread thread = new Thread(() -> { // 不停从input流获取数据 BufferedReader bufferedReader = null; try { bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String userName = Thread.currentThread().getName(); String line = ""; System.out.println("start Read"); while ((line = bufferedReader.readLine()) != null) { System.out.println(userName + "消息:" + line); for (Socket tem : socketSet) { if (!tem.equals(socket)) { PrintWriter writer = new PrintWriter(new OutputStreamWriter(tem.getOutputStream())); writer.println(line + ":" + userName); writer.flush(); } } } bufferedReader.close(); } catch (IOException e) { } System.out.println("finish Read"); }); thread.setName("用户" + userCount); thread.start(); } } }
在终端运行的效果图: