图源:
Server
package cn.icexmoon.java.note.ch25;
// ...
public class Main {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(6666);
System.out.println("server is starting...");
while (true) {
Socket client = ss.accept();
System.out.println("get connection from " + client.getRemoteSocketAddress());
dealRequest(client);
}
}
private static void dealRequest(Socket clientSocket) throws IOException {
String addr = clientSocket.getRemoteSocketAddress().toString();
try {
InputStream is = clientSocket.getInputStream();
OutputStream os = clientSocket.getOutputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
try {
do {
String msg = br.readLine();
System.out.println(String.format("[%s]received message: %s", addr, msg));
if (msg == null || msg.trim().isEmpty()) {
continue;
}
bw.write(new StringBuilder(msg).reverse().toString() + "\n");
bw.flush();
if (msg.toLowerCase().equals("bye")) {
break;
}
}
while (true);
} finally {
System.out.println("stream is closed.");
br.close();
bw.close();
}
} catch (IOException e) {
System.out.println(String.format("[%s]connect is closed by IOException.", addr));
e.printStackTrace();
} finally {
clientSocket.close();
System.out.println(String.format("[]connect is closed normally.", addr));
}
}
}
服务端主要是创建一个用于监听的Socket
,然后用Socket.accept
方法监听请求,在收到请求后进行处理。
示例是一个回文程序,在收到字符串后将其倒序回显。
需要注意的是,这里是通过换行符作为每条消息间隔进行读取和回显,而
BufferedReader.readLine
方法读取的字符串会自动去掉结尾的换行符,所以在发送时需要手动在结尾添加一个换行符。否则就会出现这边发出了消息,另一边迟迟不接收的情况。
Client
package cn.icexmoon.java.note.ch25.client;
// ...
public class Main {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 6666);
sendMsg(socket);
}
private static void sendMsg(Socket socket) throws IOException {
try {
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
BufferedReader stdReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("server is connected.");
try {
do {
System.out.println("please enter some messages:");
String msg = stdReader.readLine();
bw.write(msg + "\n");
bw.flush();
System.out.println(String.format("msg is send to server."));
System.out.println("ready to get msg back from server...");
String msgBack = br.readLine();
System.out.println("get back msg: " + msgBack);
if (msgBack.toLowerCase().equals("eyb")) {
break;
}
}
while (true);
} finally {
br.close();
bw.close();
System.out.println("stream is closed.");
}
} catch (IOException e) {
e.printStackTrace();
socket.close();
System.out.println("connect is closed caused IOException.");
} finally {
socket.close();
}
}
}
打包
需要分别打包后运行客户端和服务端,这里我选择使用mvn进行打包,具体的pom.xml
文件可以参考:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>server</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifest>
<useUniqueVersions>true</useUniqueVersions>
<addClasspath>false</addClasspath>
<mainClass>cn.icexmoon.java.note.ch25.Main</mainClass>
</manifest>
<manifestEntries>
<Class-Path>./</Class-Path>
</manifestEntries>
</archive>
<!-- 过滤掉不希望包含在jar中的文件 -->
<excludes>
<exclude>*.xml</exclude>
</excludes>
<!-- 这里不做举例了 -->
<includes>
<include>**/*.class</include>
<include>**/*.properties</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>
客户端的POM文件与之类似,只要修改入口文件类名即可。
现在只要先运行服务端jar包,再运行客户端jar包就能看到效果,这里不再详述。
多线程
上边的应用实际上是一个单线程结构,服务端每次只能接受一个客户端连接,并作出响应。
下面将这个程序改写为多线程:
package cn.icexmoon.java.note.ch25;
// ...
public class Main {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(6666);
System.out.println("server is starting...");
ExecutorService es = Executors.newCachedThreadPool();
while (true) {
Socket client = ss.accept();
final Socket finalClient = client;
es.execute(new Runnable() {
public void run() {
try {
System.out.println("get connection from " + finalClient.getRemoteSocketAddress());
dealRequest(finalClient);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
// ...
}
这里用了一个线程池来执行多线程,并且将创建连接后的响应业务代码放在Runable
中。
Java多线程相关内容可以阅读。
注意不要将
Socket client = ss.accept();
这行代码放在子线程中执行,那样会导致for
循环无限创建子线程并对端口进行监听,进而导致内存爆掉(相当刺激)。而正常逻辑应该是主线程监听,一旦有客户端连接进来,就开一个子线程进行具体服务。
现在应用可以服务多个客户端了。
这个应用实际上依然有所不足,比如客户端必须是发送>接收>再发送>再接收这样的,实际上客户端的发送和接收应当分两个线程来分别处理,回显不应当被发送所阻塞,有兴趣的童鞋可以自行修改完善。
本来打算把TCP和UDP结合写一章的,但UDP不打算写了,它并不像TCP那样应用广泛。感兴趣的可以阅读。
谢谢阅读。
最终的完整示例代码可以从获取。
文章评论