Java-Socket开发
Socket与ServerSocket通讯过程
1、Java语言对TCP协议网络通信使用java.net包中的Socket和ServerSocket进行支持;Socket被称为“套接字”,用来描述IP地址和端口;
Socket与ServerSocket的通讯过程简要描述如下:
2、java.net.Socket类中的构造方法和常用方法
方法声明 | 方法描述 |
---|---|
Socket(String host, int port) | 通过指定服务器的IP地址以及服务端口号创建Socket对象 |
InputStream getInputStream() | 返回与当前socket相关的输入流对象; |
OutputStream getOutputStream() | 返回与当前socket相关的输出流对象; |
3、java.net.ServerSocket类中的构造方法和常用方法:
方法声明 | 方法描述 |
---|---|
ServerSocket(int port) | 通过指定服务端口号创建ServerSocket对象 |
Socket accept() | 监听客户端的请求,并接受连接,返回一个Socket对象; |
import java.io.*;
public class IOUtils {
public static void close(Writer w) {
if (w != null) {
try {
w.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void close(Reader r) {
if (r != null) {
try {
r.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void close(OutputStream os) {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void close(InputStream is){
if(is!=null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketUtils {
public static void close(Socket socket){
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void close(ServerSocket serverSocket) {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void write(BufferedWriter bw, String str) throws IOException {
bw.write(str);
bw.newLine();
bw.flush();
}
public static String read(BufferedReader br) throws IOException {
//读取数据,如果没有数据可读就阻塞代码
String s = br.readLine();
System.out.println(s);
return s;
}
}
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务端,接受客户端的连接,向客户端读写数据
*/
public class Server {
public static void main(String[] args) {
ServerSocket server = null;
Socket socket = null;
InputStream inputStream = null;
BufferedReader br = null;
OutputStream outputStream = null;
BufferedWriter bw = null;
try {
server = new ServerSocket(10010);
System.out.println("服务器启动成功");
System.out.println("等待客户端连接");
//调用ServerSocket的accept方法,等待并可以接受客户端的请求,并返回当前的Socket对象
//阻塞代码
socket = server.accept();
System.out.println("接收到客户端的连接,Ip地址:" + socket.getInetAddress().getHostAddress());
//获取输入流
inputStream = socket.getInputStream();
br = new BufferedReader(new InputStreamReader(inputStream));
outputStream = socket.getOutputStream();
bw = new BufferedWriter(new OutputStreamWriter(outputStream));
//一直循环读取数据
while (true) {
//读取数据,如果没有数据可读就阻塞代码
String s = SocketUtils.read(br);
//数据写回客户端
SocketUtils.write(bw, "服务器返回的数据:" + s);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.close(bw);
IOUtils.close(br);
IOUtils.close(outputStream);
IOUtils.close(inputStream);
SocketUtils.close(socket);
SocketUtils.close(server);
}
}
}
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* 连接服务器端,向服务器读写数据
*/
public class Client {
public static void main(String[] args) {
Socket socket = null;
InputStream inputStream = null;
BufferedReader br = null;
OutputStream outputStream = null;
BufferedWriter bw = null;
try {
System.out.println("客户端启动,准备连接服务器");
socket = new Socket("192.168.140.254", 10010);
System.out.println("连接服务器成功,准备发送数据");
outputStream = socket.getOutputStream();
bw = new BufferedWriter(new OutputStreamWriter(outputStream));
inputStream = socket.getInputStream();
br = new BufferedReader(new InputStreamReader(inputStream));
Scanner scanner = new Scanner(System.in);
System.out.println("请输入数据:");
while (true) {
//接收控制台输入的数据阻塞代码
String s = scanner.next();
SocketUtils.write(bw, s);
//读取数据
SocketUtils.read(br);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.close(bw);
IOUtils.close(br);
IOUtils.close(outputStream);
IOUtils.close(inputStream);
SocketUtils.close(socket);
}
}
}
4、上述案例中,运行多个client,结果如?
idea设置可以启动多个client
运行多个client只有一个client能够发送消息
TCP通讯线程特性
1、基于TCP协议的通讯,客户端和服务器端都使用Socket对象获取输入流和输出流;使用IO流对象读写数据进行通讯;
2、ServerSocket的accept方法是阻塞的,当服务器端接受了一个客户端请求建立连接后,就不会为其他客户端服务;如果需要服务器端为多个客户端服务,必须为每个客户端启动一个新的线程;
3、ThreadServer类负责处理客户端的请求
import java.io.*;
import java.net.Socket;
public class ThreadServer extends Thread{
private Socket socket;
public ThreadServer(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream inputStream = null;
BufferedReader br = null;
OutputStream outputStream = null;
BufferedWriter bw = null;
//获取输入流
try {
//获取输入流
inputStream = socket.getInputStream();
br = new BufferedReader(new InputStreamReader(inputStream));
//获取输出流
outputStream = socket.getOutputStream();
bw = new BufferedWriter(new OutputStreamWriter(outputStream));
//一直循环读取数据
while (true) {
//读取数据,如果没有数据可读就阻塞代码
String s = read(br);
//数据写回客户端
write(bw, "服务器返回的数据:" + s);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.close(bw);
IOUtils.close(br);
IOUtils.close(outputStream);
IOUtils.close(inputStream);
SocketUtils.close(socket);
}
}
/*
* 读
*/
private static String read(BufferedReader br) {
try {
//读取数据,如果没有数据可读就阻塞代码
String s = br.readLine();
System.out.println("服务器端接收到的数据:" + s);
return s;
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
/*
* 写
*/
private static void write(BufferedWriter bw, String str) {
try {
bw.write(str);
bw.newLine();
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server1 {
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(10010);
System.out.println("服务器启动成功");
System.out.println("等待客户端连接");
//调用ServerSocket的accept方法,等待并可以接受客户端的请求,并返回当前的Socket对象
//阻塞代码
while (true) {
Socket socket = server.accept();
System.out.println("接收到客户端的连接,Ip地址:" + socket.getInetAddress().getHostAddress());
//启动线程,处理客户端和服务端通讯
new ThreadServer(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
SocketUtils.close(server);
}
}
}
4、结论
在一个线程里处理多个客户端请求会发生阻塞问题;
如果要同时服务多个客户端,则必须为每个客户端启动一个线程提供服务;
单一服务器对多客户端提供网络服务
- 实际编程中,我们通常需要一个服务器对多个客户端提供网络服务;
- 要实现一个服务器对多个客户端提供网络服务,则在接受请求后,启动一个独立线程为当前客户端服务即可;
Java nio实现无阻塞访问的Socket服务器
- 之前的IO流章节已经介绍,JDK1.5以后io包的很多功能已经通过nio的方式进行了重构,因此io操作本身的性能已经得到了极大的提升,但是nio和传统的io还有一个极大的差异:nio能够以非阻塞的形式完成数据读取
- 以事件循环的方式读取数据而不是传统的阻塞方式读取数据带来的好处是无需在服务器端构建多余的线程来等待数据从而减轻服务器的性能负担
- 构建nio版本的Socket服务器的基本过程是:
- 开启ServerSocket通道并绑定一个TCP端口
- 创建nio事件选择器并注册到ServletSocket
- 循环处理选择器中关联的Socket事件(isAcceptable-有用户连接,isReadable-有数据可供读取)
- Selector选择器中有2个表:
- 登记表:当我们调用时
channel.register
,会记录通道对应的感兴趣的事件。仅当我们调用时key.cancel()
,它将从此表中删除。 - 准备好选择表:当我们调用时
selector.select()
,选择器将查找登记表,找到可用的键,并将它们的引用复制到该选择表中。选择器不会清除此表中的通道感兴趣的事件(这意味着,即使我们selector.select()
再次调用,它也不会清除现有项目)
- 登记表:当我们调用时
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/*
NIO服务端
*/
public class NIOServer {
//通道管理器
private Selector selector;
public static void main(String[] args) throws IOException {
//new NIOServer().init(10010).listen();
NIOServer nioServer = new NIOServer();
nioServer = nioServer.init(10020);
nioServer.listen();
}
/*
初始化NIOServer
*/
private NIOServer init(int port) throws IOException {
//获取ServerSocketChannel通道
ServerSocketChannel sc = ServerSocketChannel.open();
//设置非阻塞模式
sc.configureBlocking(false);
//设置通道绑定的端口
sc.socket().bind(new InetSocketAddress(port));
//获取通道管理器
selector = Selector.open();
//将通道管理器与通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件
sc.register(selector, SelectionKey.OP_ACCEPT);
//只有当该事件到达时,Selector.select()会返回,否则一直阻塞
return this;
}
/*
监听客户端的连接和发送数据
*/
private void listen() throws IOException {
System.out.println("服务端启动成功,开始监听客户端连接");
//循环处理 客户端连接事件和可读事件 使用轮训访问selector
while(true){
//abstract int select()
/*
selector对象(内部循环监听注册再selector的通道,感兴趣的事件触发)
如果没有感兴趣的事件触发,就会阻塞代码。
【当有注册的事件到达时,方法返回,否则阻塞。】
*/
selector.select();
//处理事件 返回此选择器的选择键集。
Set<SelectionKey> selectionKeys = selector.selectedKeys();/*abstract Set<SelectionKey> selectedKeys()*/
//获取迭代器
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//循环处理(被触发的事件,可能有多个)
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//删除已经选择的key,防止重复处理
iterator.remove();
//客户端请求连接事件
if (key.isAcceptable()) {
/*返回创建此键的通道。 该方法即使在取消键之后仍将继续返回通道。*/
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
// 获得客户端连接通道,返回SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
/*final SelectableChannel configureBlocking(boolean block)
block - 如果true那么这个通道将被置于阻塞模式
如果false那么它将被放置为非阻塞模式
*/
socketChannel.configureBlocking(false);
//向客户端发消息
socketChannel.write(ByteBuffer.wrap(new String("hello world").getBytes()));
/*
SocketChannel对象注期到selector对象中,感兴趣的事件是读事件
【在与客户端连接成功后,为客户端通道注册SelectionKey.OP_READ事件。】
*/
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端请求连接事件");
}else if (key.isReadable()) {// 有可读数据事件
/*获取客户端传输数据可读取消息通道。*/
SocketChannel channel = (SocketChannel) key.channel();
/*static ByteBuffer allocate(int capacity)
创建读取数据缓冲器,分配一个新的字节缓冲区。
capacity - 新的缓冲区的容量,以字节为单位
*/
ByteBuffer buffer = ByteBuffer.allocate(1024);
//一次性全部读取
int read = channel.read(buffer);
byte[] data = buffer.array();
String str = new String(data,0,data.length);
System.out.println("receive message from client, size:" + buffer.position() + " 接收到客户端发送来的数据: " + str);
//向客户端发消息
channel.write(ByteBuffer.wrap(new String("发送的数据:"+str).getBytes()));
}else{
System.out.println("其他事件被触发了,暂不处理,"+key);
}
}
}
}
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
/*
NIO客户端
*/
public class NIOClient {
// 管道管理器
private Selector selector;
public static void main(String[] args) throws IOException {
new NIOClient().init("192.168.140.177", 10020).start();
}
public NIOClient init(String serverIp, int port) throws IOException {
// 获取socket通道对象
SocketChannel channel = SocketChannel.open();
//设置非阻塞
channel.configureBlocking(false);
// 获得通道管理器
selector = Selector.open();
// 客户端连接服务器,需要调用channel.finishConnect();才能实际完成连接。
/*abstract boolean connect(SocketAddress remote)
参数 remote - 要连接该通道的远程地址
结果 true如果连接建立, false如果该通道处于非阻塞模式并且连接操作正在进行中
*InetSocketAddress(String hostname, int port)
参数 hostname - 主机名 port - 端口号
结果 创建Socket地址
*/
channel.connect(new InetSocketAddress(serverIp, port));
// 为该通道注册SelectionKey.OP_CONNECT事件
/*final SelectionKey register(Selector sel, int ops)
参数 sel - 要注册该频道的选择器 ops - 为结果键设置的兴趣
结果 表示该通道与给定选择器的注册的键
*/
channel.register(selector, SelectionKey.OP_CONNECT/*用于Socket连接操作的操作集位。 */);
return this;
}
public void start() throws IOException {
Scanner sc = new Scanner(System.in);
System.out.println("客户端启动");
// 轮询访问selector
while (true) {
// 选择注册过的io操作的事件(第一次为SelectionKey.OP_CONNECT)
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> ite = selectionKeys.iterator();
while (ite.hasNext()) {
SelectionKey key = ite.next();
// 删除已选的key,防止重复处理
ite.remove();
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
// 如果正在连接,则完成连接
if (channel.isConnectionPending()) {
channel.finishConnect();
}
channel.configureBlocking(false);
// 向服务器发送消息
channel.write(ByteBuffer.wrap("send message to server:i am client".getBytes()));
// 连接成功后,注册接收服务器消息的事件
channel.register(selector, SelectionKey.OP_READ/*读操作的操作位。*/);
System.out.println("客户端连接成功");
}
//有可读数据事件。
else if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//把通道的数据读到ByteBuffer对象中
channel.read(buffer);
byte[] data = buffer.array();
String str = new String(data);
System.out.println("接收到服务端发过来的数据:,size:" + buffer.position() + " ,str: " + str);
/*ByteBuffer outBuffer =
ByteBuffer.wrap(("client.".concat(str)).getBytes());
channel.write(outBuffer);*/
//阻塞代码
String s = sc.next();
channel.write(ByteBuffer.wrap(s.getBytes()));
} else {
System.out.println("其他的事件被触发:" + key);
}
}
}
}
}