服务端ServerSocket和客户端Socket之间通过Socket建立连接和通信。首先ServerSocket将在服务端监听某个端口,当发现客户端有Socket来试图连接它时,它会accept该Socket的连接请求,同时在服务端建立一个对应的Socket与之进行通信。
服务端往Socket的输出流里面写东西,客户端就可以通过Socket的输入流读取对应的内容。Socket与Socket之间是双向连通的,所以客户端也可以往对应的Socket输出流里面写东西,然后服务端对应的Socket的输入流就可以读出对应的内容。
为了实现多线程,ServerSocket采用while(true)语句,调用其accept方法试图接收来自客户端的连接请求。当没有接收到请求的时候,程序会在这里阻塞直到接收到来自客户端的连接请求,之后会跟当前建立好连接的客户端进行通信,完了后会接着执行循环体再次尝试接收新的连接请求。每次ServerSocket接收到一个新的Socket连接请求后都会新起一个线程来跟当前Socket进行通信,这样就达到了异步处理与客户端Socket进行通信的情况。
BufferedReader的readLine方法是一次读一行的,这个方法是阻塞的,直到它读到了一行数据为止程序才会继续往下执行,那么readLine什么时候才会读到一行呢?直到程序遇到了换行符或者是对应流的结束符readLine方法才会认为读到了一行,才会结束其阻塞,让程序继续往下执行。所以我们在使用BufferedReader的readLine读取数据的时候一定要记得在对应的输出流里面一定要写入换行符(流结束之后会自动标记为结束,readLine可以识别),写入换行符之后一定记得如果输出流不是马上关闭的情况下记得flush一下,这样数据才会真正的从缓冲区里面写入。
在服务端代码中我们在定义输入流的时候明确定义了使用GBK编码来读取数据,而在定义输出流的时候明确指定了将使用UTF-8编码来发送数据。如果客户端上送数据的时候不以GBK编码来发送的话服务端接收的数据就很有可能会乱码;同样如果客户端接收数据的时候不以服务端发送数据的编码,即UTF-8编码来接收数据的话也极有可能会出现数据乱码的情况。所以要保持发送和接收的数据一致性。
Client.java:
package com.sun;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;
import java.net.SocketTimeoutException;
public class Client {
public static void main(String args[]) throws Exception {//异常判断是主要为了确定是否能连接上,比如当端口号被占用时应当抛出异常
String host = "127.0.0.1"; //要连接的服务端IP地址,这里为了便于测试,选择本地IP
int port = 9000; //要连接的服务端对应的监听端口,可任意选择
Socket client = new Socket(host, port); //与服务端建立连接
Writer writer = new OutputStreamWriter(client.getOutputStream(), "GBK"); //建立连接后就可以往服务端写数据,设置数据格式为"GBK"
writer.write("Hello,Server.");//写入要传输给服务器的数据
writer.write("eof\n");//每次用eof作为结束标志符
writer.flush();//记住要flush一下,只有这样服务端才能收到客户端发送的数据,否则可能会引起两边无限的互相等待。
BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); //读服务端的数据,并设置编码格式为"UTF-8"
int timeout = 10;
client.setSoTimeout(timeout * 1000);//Socket为我们提供了一个setSoTimeout()方法来设置接收数据的超时时间,单位是毫秒。
StringBuffer sb = new StringBuffer();//StringBuffer用来连接收到的字符串
String temp;//BufferedReader提供了缓存,使用readLine来一次读一行,提高了读的效率,temp用来记录每一行的内容
int index;//判断是否读到最后
try {
while ((temp = br.readLine()) != null) {
if ((index = temp.indexOf("eof")) != -1) {//若读到最后,记录下后就break
sb.append(temp.substring(0, index));
break;
}
sb.append(temp);//连接字符串
}
} catch (SocketTimeoutException e) {
System.out.println("time out");//因为设置了超时时间,当超时时需抛出异常
}
System.out.println("server: " + sb);
writer.close();//close的顺序要注意,必须从后往前关闭
br.close();
client.close();
}
}
Server.java:
package com.sun;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String args[]) throws IOException {//异常判断是主要为了确定是否能连接上,比如当端口号被占用时应当抛出异常
int port = 9000;//定义一个ServerSocket监听在端口9000上
ServerSocket server = new ServerSocket(port);//创建一个端口号为9000的ServerSocket
while (true) {//服务端一直保持监听状态
Socket socket = server.accept();//server尝试接收其他Socket的连接请求,server的accept方法是阻塞式的
new Thread(new Task(socket)).start(); //每接收到一个Socket就建立一个新的线程,并启动该线程
}
}
/**
* Task:处理接受到Socket请求的类
*/
static class Task implements Runnable {//Task类实现Runnnable接口
private Socket socket;//拥有Socket成员
public Task(Socket socket) {
this.socket = socket;
}
public void run() {
try {
handleSocket();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 跟客户端Socket进行通信
*
* @throws Exception
*/
private void handleSocket() throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "GBK"));//读客户端的数据,并设置编码格式为"GBK"
StringBuilder sb = new StringBuilder();
String temp;
int index;
while ((temp = br.readLine()) != null) {
System.out.println(temp);
if ((index = temp.indexOf("eof")) != -1) {//遇到eof时就结束接收
sb.append(temp.substring(0, index));
break;
}
sb.append(temp);
}
System.out.println("Client: " + sb);
Writer writer = new OutputStreamWriter(socket.getOutputStream(), "UTF-8");
writer.write("Hello,Client");
writer.write("eof\n");
writer.flush();
writer.close();
br.close();
socket.close();//socket别忘了关闭
}
}
}
在命令行中,用javac进行编译生成两个class文件
注意:在Java Server或Java Client时,因为上述代码中加入了包package com.sun,所以cd 要退回到com的上一级目录,用全称java com.sun.Server和java com.sun.Client才可以,否则会报错:找不到或无法加载主类包名。
保证port不被占用,Server程序打开一个
Client程序打开多个