前言
学习Java网络编程的过程中,一开始都是利用Java的原生Socket来练手的,后面才知道它们的I/O模型属于BIO模型,即同步阻塞I/O模型。下面利用原生socket实现TCP连接通信和UDP通信的demo。
Socket编程
socket是操作系统提供的网络编程接口,它封装了TCP/IP协议栈的支持,属于应用层和传输层之间的API,用于进程间的通信。当有连接接入主机时,操作系统会自动为其分配一个socket,socket绑定着一个ip+port。通过该socket,可以获得tcp连接的输入流和输出流,本机的进程就可以与远程进程进行通信,进行读取写入操作。
Java提供的net包可用于Socket编程。使用socket绑定一个ip+port,用于客户端的请求处理和发送;使用Serversocket绑定本地ip和port,用于服务端的TCP请求接收。
UDP的客户端和服务端实现起来比TCP简单,由于udp数据报的长度是确定的,只需要写入一个固定的缓存空间和读取一个固定的缓存空间就可以了。使用DatagramPacket封装数据报,使用DatagramSocket收发数据报。
TCP通信
客户端
package BIO.TCP;
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* 使用java原生Socket实现TCP连接传输
*/
public class TCPClient {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Socket s = new Socket("192.168.1.155", 8111);
//构建IO
InputStream is = s.getInputStream();
OutputStream os = s.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
//向服务器端发送一条消息
//注意要带换行符!!!
bw.write("你好,我是客户端" + "\n\r");
bw.flush();
//读取服务器返回的消息
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String mess = br.readLine();
System.out.println("服务器:" + mess);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
服务端
package BIO.TCP;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
ServerSocket ss = new ServerSocket(8111);
while (true) {
System.out.println("启动服务器....");
Socket s = ss.accept();
System.out.println("客户端:" + s.getInetAddress().getLocalHost() + "已连接到服务器");
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
//读取客户端发送来的消息
String mess = br.readLine();
System.out.println("客户端:" + mess);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
//注意要带换行符!!!
bw.write("你好,我是服务器"+"\n\r" );
bw.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}}
UDP通信
客户端
package BIO.UDP;
import java.net.*;
public class UDPClient {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
DatagramSocket socket = new DatagramSocket();
//要发送的内容
byte[] bytes = "你好,我是客户端".getBytes();
InetAddress host= InetAddress.getByName("192.168.1.155");
int port = 8111;
DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, bytes.length, host, port);
socket.send(datagramPacket);
} catch (java.io.IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
服务端
package BIO.UDP;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* 使用java原生Socket实现UDP传输
*/
public class UDPServer {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
DatagramSocket socket=new DatagramSocket(8111);
byte[] buffer=new byte[1024];
DatagramPacket datagramPacket=new DatagramPacket(buffer,buffer.length);
socket.receive(datagramPacket);
System.out.println("服务器接收:");
String msg=new String(datagramPacket.getData(),"utf-8");
System.out.println(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
总结与优化
在上文的实现中,I/O流的读取和写入操作会发生阻塞的情况:
//读取客户端发送来的消息
String mess = br.readLine();
当客户端一直未发送消息时,服务端就会阻塞在readline方法上面。
所以当多个客户端对服务器进行请求时,服务器只能一个个串行处理,这在响应时间上肯定不能达标。
-
优化1 :一个soket对应一个线程
对每个客户端的请求,在服务器上都单开一个线程进行处理。
缺点:当并发量很大时,由于每个线程都会占据一个文件句柄,而服务器上的句柄数是有限的,同时大量的线程间切换也会造成很大的消耗。所以并发量大的场景下一定是承载不住的。 -
优化2:多线程+线程池
既然不能无限创建线程,那么利用线程池限制线程数量不就行了,即只启动固定的线程数来对socket通信进行处理。
public class ThreadPoolApplication {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(8);
for (; ;) {
Thread t = new Thread(new Runnable() {
public void run() {
//对socket通信的处理
}
});
executorService.submit(t);
}
}
}
优化方式2的处理办法看起来是最优的了,但是有没有更好的呢? 上面优化的两种方式在socket输入输出数据没有准备好的情况下线程都会阻塞,如果一个线程能够同时处理多个socket通信而且在socket输入输出数据没有准备好的情况下不会发生阻塞,那岂不是更优? 这种技术就叫做I/O多路复用, 在Java的nio包中提供了实现。