架构图
无论是基于socket,还是基于nio channnel,架构是一样的。
最关键的一点就是,服务器端在接受客户端socket连接之后,会为每个客户端socket创建一个专门的socket,以便和客户端socket进行一一通信。

关于端口
1.服务器端
1)服务器套接字 //固定端口,作用是监听客户端连接
2)和客户端通信的套接字 //接收到客户端连接之后,创建专门的socket(随机分配,一般是万级别)与当前客户端进行通信
服务器端有两个socket,作用不同,port也不同。而且2)的socket对每个客户端连接都是一个新的socket。
2.客户端
1)客户端连接服务器的ip和port //服务器ip和port
2)客户端socket自己本身的port //客户端socket自己本身的端口(随机分配,一般是万级别)
客户端只有一个socket,除了自己的port,还有要绑定连接到服务器的ip/port。
最简单的socket程序-服务器只接受一次客户端连接
服务器
package socket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 最简单的socket程序-只接受一次连接
* <pre>
* @author gzh
* @date 2019年7月25日 下午9:06:13
* </pre>
*/
public class ServerMain {
public static void main(String[] args) {
try {
//创建服务器socket
ServerSocket serverSocket = new ServerSocket(8080); //一个socket对应一个ip/port,计算机io主要是两块1.磁盘io,即文件io 2.网络io,即socket io。socket io和磁盘文件io的区别是什么?socket io需要提供端口,磁盘文件io不需要port,因为磁盘文件io是在本地,而socket io是网络通信,也就是所谓的网络编程。当前服务器端程序在port 8080上监听。
//接受客户端连接
Socket socket = serverSocket.accept(); //接受客户端连接成功之后,返回(其实是创建)一个新的socket。这个socket又占了一个新的port,不信你可以打印出来看下。如何打印socket port?有API方法。但是没有设置port啊,port从哪里来的?随机设置了一个,而且你会发现随机设置的一般都是几万,目的是为了避免和其他端口冲突。
//读数据
InputStream read = socket.getInputStream(); //基于socket,既可以读数据(即可以获取读流),又可以写数据(即可以获取写流)
int data_read = read.read(); //读一个字节
System.out.println(data_read);
//写数据
OutputStream write = socket.getOutputStream();
int data_write = 2;
write.write(data_write);
} catch (IOException e) {
e.printStackTrace();
}
//程序不关闭,目的是有时间打印读写数据
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
客户端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
/**
* 最简单的客户端socket
* <pre>
* @author gzh
* @date 2019年7月25日 下午10:20:47
* </pre>
*/
public class ClientMain {
public static void main(String[] args) {
Socket socket;
try {
//创建客户端socket
socket = new Socket("127.0.0.1", 8080); //Creates a stream socket and connects it to the specified port number on the named host.参数是服务器的ip/port,目的是连接到服务器ip/port。另外,客户端socket的port是随机分配的。
//连接服务器socket
// InetSocketAddress address = new InetSocketAddress(8080);
// socket.connect(address); //连接到服务器port8080 //这样写连接不上,必须显式写ip地址
//写数据
OutputStream write = socket.getOutputStream();
write.write(1);
//读数据
InputStream read = socket.getInputStream();
int data_read = read.read(); //只读一个字节
System.out.println(data_read);
} catch (UnknownHostException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
//程序不关闭
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果
1.服务器:1
2.客户端:2
最简单的socket程序-循环接受客户端连接
服务器
package socket2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 最简单的socket程序-循环接受客户端连接
* <pre>
* @author gzh
* @date 2019年7月25日 下午9:06:13
* </pre>
*/
public class ServerMain {
public static void main(String[] args) {
//创建服务器socket
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(8080); //服务器socket只需要创建一次即可,也就是说,只需要创建一个服务器socket对象,该socket对象始终监听某个固定端口
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} //一个socket对应一个ip/port,计算机io主要是两块1.磁盘io,即文件io 2.网络io,即socket io。socket io和磁盘文件io的区别是什么?socket io需要提供端口,磁盘文件io不需要port,因为磁盘文件io是在本地,而socket io是网络通信,也就是所谓的网络编程。当前服务器端程序在port 8080上监听。
while(true) {
try {
//接受客户端连接
Socket socket = serverSocket.accept(); //接受客户端连接成功之后,返回(其实是创建)一个新的socket。这个socket又占了一个新的port,不信你可以打印出来看下。如何打印socket port?有API方法。但是没有设置port啊,port从哪里来的?随机设置了一个,而且你会发现随机设置的一般都是几万,目的是为了避免和其他端口冲突。
//读数据
InputStream read = socket.getInputStream(); //基于socket,既可以读数据(即可以获取读流),又可以写数据(即可以获取写流)
int data_read = read.read(); //读一个字节
System.out.println(data_read);
//写数据
OutputStream write = socket.getOutputStream();
int data_write = 2;
write.write(data_write);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端
package socket2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
/**
* 最简单的客户端socket
* <pre>
* @author gzh
* @date 2019年7月25日 下午10:20:47
* </pre>
*/
public class ClientMain {
public static void main(String[] args) {
Socket socket;
while(true) {
try {
//创建客户端socket
socket = new Socket("127.0.0.1", 8080); //客户端socket的port是随机分配的,而且每次随机分配的port值不一样
//连接服务器socket
// InetSocketAddress address = new InetSocketAddress(8080);
// socket.connect(address); //连接到服务器port8080 //这样写连接不上,必须显式写ip地址
//写数据
OutputStream write = socket.getOutputStream();
write.write(1);
//读数据
InputStream read = socket.getInputStream();
int data_read = read.read(); //只读一个字节
System.out.println(data_read);
} catch (UnknownHostException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
输出结果
1.服务器:多个1
2.客户端:多个2
最简单的socket程序-多线程
服务器
package socket3;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 最简单的socket程序-循环接受客户端连接,但是这次使用多线程处理每个客户端连接,目的是并发连接可以被多线程同时执行,而不是一个连接一个连接的执行,后面的连接必须等待前面的连接执行完成。
*
* 怎么实现?就是把socket对象丢到线程里去即可。
* <pre>
* @author gzh
* @date 2019年7月25日 下午9:06:13
* </pre>
*/
public class ServerMain {
public static void main(String[] args) {
//创建服务器socket
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(8080); //服务器socket只需要创建一次即可,也就是说,只需要创建一个服务器socket对象,该socket对象始终监听某个固定端口
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} //一个socket对应一个ip/port,计算机io主要是两块1.磁盘io,即文件io 2.网络io,即socket io。socket io和磁盘文件io的区别是什么?socket io需要提供端口,磁盘文件io不需要port,因为磁盘文件io是在本地,而socket io是网络通信,也就是所谓的网络编程。当前服务器端程序在port 8080上监听。
while(true) {
try {
//接受客户端连接
Socket socket = serverSocket.accept(); //接受客户端连接成功之后,返回(其实是创建)一个新的socket。这个socket又占了一个新的port,不信你可以打印出来看下。如何打印socket port?有API方法。但是没有设置port啊,port从哪里来的?随机设置了一个,而且你会发现随机设置的一般都是几万,目的是为了避免和其他端口冲突。
Thread thread = new Thread(new TaskConnection(socket));
thread.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
package socket3;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class TaskConnection implements Runnable {
Socket socket;
TaskConnection(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//读数据
InputStream read = socket.getInputStream(); //基于socket,既可以读数据(即可以获取读流),又可以写数据(即可以获取写流)
int data_read = read.read(); //读一个字节
System.out.println(data_read + ", Thread.currentThread().getName():" + Thread.currentThread().getName());
//写数据
OutputStream write = socket.getOutputStream();
int data_write = 2;
write.write(data_write);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/*
* Runnable 怎么获取线程的名字?
* 获取线程名字这件事情本质上和Runnable是没有关系的。一个Runnable可以给多个线程去运行,所以如果在这个概念上你有误解的话,希望重新考虑一下。
* 另外,在任何时候,你都可以用Thread.currentThread().getName()来获取当前线程的名字
*/
客户端
package socket3;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
/**
* 最简单的客户端socket
* <pre>
* @author gzh
* @date 2019年7月25日 下午10:20:47
* </pre>
*/
public class ClientMain {
public static void main(String[] args) {
Socket socket;
while(true) {
try {
//创建客户端socket
socket = new Socket("127.0.0.1", 8080); //客户端socket的port是随机分配的,而且每次随机分配的port值不一样
//连接服务器socket
// InetSocketAddress address = new InetSocketAddress(8080);
// socket.connect(address); //连接到服务器port8080 //这样写连接不上,必须显式写ip地址
//写数据
OutputStream write = socket.getOutputStream();
write.write(1);
//读数据
InputStream read = socket.getInputStream();
int data_read = read.read(); //只读一个字节
System.out.println(data_read);
} catch (UnknownHostException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
输出结果
1.服务器:多个1 //另外,还输出线程名字
1, Thread.currentThread().getName():Thread-0
1, Thread.currentThread().getName():Thread-1
1, Thread.currentThread().getName():Thread-2
1, Thread.currentThread().getName():Thread-3
1, Thread.currentThread().getName():Thread-4
1, Thread.currentThread().getName():Thread-5
1, Thread.currentThread().getName():Thread-6
1, Thread.currentThread().getName():Thread-7
......
2.客户端:多个2
多路复用-没有使用XXXHandler
服务器
package selector;
/**
* 服务器入口:启动单线程
* <pre>
* @author gzh
* @date 2019年3月20日 上午10:33:27
* </pre>
*/
public class Server {
public static void main(String[] args) {
// 单线程处理任务
new Thread(new Task1()).start();
;
}
}
package selector;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* 单线程处理连接
* <pre>
* @author gzh
* @date 2019年3月20日 上午11:22:42
* </pre>
*/
public class Task1 implements Runnable {
private Selector selector = null;
@Override
public void run() {
//创建服务器端套接字通道
ServerSocketChannel serverSocketChannel = null;
try {
serverSocketChannel = ServerSocketChannel.open(); //jdk nio基于通道通信。channel和socket通信的区别是什么?socket同时只能读或写,单向通信;而channel可以同时读和写。底层是硬件-网线就支持双向通信,有的线专门读,有的线专门写。总结,相同点,作用都是通信;不同点是同一时刻一个是只能单向通信一个可以是双向通信,除此之外,没有其他不同的地方。
//这里得到(其实是创建)一个channel对象,和socket里的创建socket对象是一模一样的
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//监听客户端连接的端口
try {
serverSocketChannel.socket().bind(new InetSocketAddress(8080),1024); //服务器监听固定port 8080
//为什么要根据channel获取socket对象?那channel和socket到底是什么关系?既然最终还是要获取socket对象,为什么要多搞一个channel出来?
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
//非阻塞
try {
serverSocketChannel.configureBlocking(false);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//创建多路选择器
try {
selector = Selector.open();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//关联服务器套接字通道和多路选择器
try {
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //监听连接事件
} catch (ClosedChannelException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//一直循环轮询
while (true) {
//遍历事件
try {
selector.select(1000); //阻塞轮询,直到有新的事件到来
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Set set = selector.selectedKeys(); //处理事件
Iterator iterator = set.iterator();
while (iterator.hasNext()) { //遍历处理事件集合
//取事件
SelectionKey selectionKey = (SelectionKey) iterator.next();
//处理事件
handleEvent(selectionKey); //也可以使用XXXHandler处理事件
}
}
}
/**
* 处理事件
* <pre>
* 1.连接
* 2.读
* 3.写
* @author gzh
* @date 2019年3月20日 上午10:49:23
* @param selectionKey
* </pre>
*/
private void handleEvent(SelectionKey selectionKey) {
if (selectionKey.isAcceptable()) { //连接事件
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
try {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ); //监听可读事件
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else if (selectionKey.isReadable()) { //读事件
SocketChannel socketChannel = (SocketChannel)selectionKey.channel(); //获取当前事件的channel(这里其实和socket的思路是完全一致的,如果是socket,服务器接受客户端连接之后也是创建一个专门的socket和对应客户端通信,这个是一一对应的,即客户端连接(也是socket,有自己的端口)和服务器端的socket,有自己的端口)一一对应,基于这个原理才能保证服务器和客户端一对一的正常通信。
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
int n = socketChannel.read(byteBuffer); //读客户端写过来的数据
byte[] bytes = new byte[n];
byteBuffer.get(bytes);
String string = new String(bytes, "utf-8");
System.out.println(string);
//写数据到客户端
doWrite(socketChannel);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* 写数据到客户端
* <pre>
* @author gzh
* @date 2019年3月20日 上午11:13:15
* @param socketChannel
* </pre>
*/
private void doWrite(SocketChannel socketChannel) {
String string = "服务器端写数据到客户端";
byte[] bytes = null;
try {
bytes = string.getBytes("utf-8");
} catch (UnsupportedEncodingException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);
byteBuffer.put(bytes);
try {
socketChannel.write(byteBuffer); //写数据到客户端(因为服务器是同一个channel,所以写和刚才的读都是和同一个客户端socket通信)
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
客户端
package selector;
/**
* 客户端入口:启动客户端单线程
* <pre>
* @author gzh
* @date 2019年3月20日 下午1:35:36
* </pre>
*/
public class Client {
public static void main(String[] args) {
new Thread(new Task2());
}
}
package selector;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* 客户端单线程
* <pre>
* @author gzh
* @date 2019年3月20日 下午1:35:51
* </pre>
*/
public class Task2 implements Runnable {
Selector selector = null;
@Override
public void run() {
try {
//创建客户端套接字通道
SocketChannel socketChannel = SocketChannel.open();
//非阻塞模式
socketChannel.configureBlocking(false);
//连接服务器
boolean b = socketChannel.connect(new InetSocketAddress(8080)); //因为异步,所以两种结果:1.立即返回true 2.暂时false
//创建多路选择器
selector = Selector.open();
//1.处理立即成功的情况
//判断连接是否成功
if (b) { //连接成功,读事件
//读事件
socketChannel.register(selector, SelectionKey.OP_READ);
//写数据到服务器
doWrite(socketChannel);
} else { //不成功,连接事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
//2.处理暂时false的情况
//一直循环轮询
while (true) {
//遍历事件
try {
selector.select(1000);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Set set = selector.selectedKeys();
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
//取事件
SelectionKey selectionKey = (SelectionKey) iterator.next();
//处理事件
handleEvent(selectionKey);
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 写数据到服务器
* <pre>
* @author gzh
* @param socketChannel
* @date 2019年3月20日 下午1:45:05
* </pre>
*/
private void doWrite(SocketChannel socketChannel) {
//写数据到服务器
}
/**
* 处理事件
* <pre>
* @author gzh
* @date 2019年3月20日 下午1:38:53
* @param selectionKey
* </pre>
*/
private void handleEvent(SelectionKey selectionKey) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//连接成功事件
if (selectionKey.isConnectable()) {
try {
if (socketChannel.finishConnect()) { //连接成功
//读事件
socketChannel.register(selector, SelectionKey.OP_READ);
//写数据到服务器
doWrite(socketChannel);
}else { //程序退出
System.exit(1);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//读事件
if (selectionKey.isReadable()) {
//读服务器发送的数据
}
}
}
说明
java nio-多路复用选择器的demo。
参考
李林锋《netty权威指南》