最近在整理socket知识,特意做了一个笔记,以供以后查阅。
1.简介
IP协议对应于网络层,TCP协议对应于传输层,而HTTP协议对应于应用层,socket则是对TCP/IP协议的封装和应用(程序员层面上),它并不是一个协议。
在TCP/IP协议族中主要的Socket类型为流套接字(streamsocket)和数据报套接字(datagramsocket)。流套接字将TCP作为其端对端协议,据报套接字使用UDP协议。
2.通信模型
3.Android中的源码实现
3.1 TCP模式
Java中基于TCP协议实现网络通信的类:
- 客户端的Socket类
- 服务器端的ServerSocket类
3.1.1 TCP通信实现步骤
① 创建ServerSocket和Socket
② 打开连接到Socket的输入/输出流
③ 按照协议对Socket进行读/写操作
④ 关闭输入输出流、关闭Socket
服务器端:
① 创建ServerSocket对象,绑定监听端口
② 通过accept()方法监听客户端请求
③ 连接建立后,通过输入流读取客户端发送的请求信息
④ 通过输出流向客户端发送响应信息
⑤ 关闭相关资源
/**
* 基于TCP协议的Socket通信,实现用户登录,服务端
*/
//1、创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
ServerSocket serverSocket =newServerSocket(10086);//1024-65535的某个端口
//2、调用accept()方法开始监听,等待客户端的连接
Socket socket = serverSocket.accept();
//3、获取输入流,并读取客户端信息
InputStream is = socket.getInputStream();
InputStreamReader isr =newInputStreamReader(is);
BufferedReader br =newBufferedReader(isr);
String info =null;
while((info=br.readLine())!=null){
System.out.println("我是服务器,客户端说:"+info);
}
socket.shutdownInput();//关闭输入流
//4、获取输出流,响应客户端的请求
OutputStream os = socket.getOutputStream();
PrintWriter pw = new PrintWriter(os);
pw.write("欢迎您!");
pw.flush();
//5、关闭资源
pw.close();
os.close();
br.close();
isr.close();
is.close();
socket.close();
serverSocket.close();
客户端:
① 创建Socket对象,指明需要连接的服务器的地址和端口号
② 连接建立后,通过输出流想服务器端发送请求信息
③ 通过输入流获取服务器响应的信息
④ 关闭响应资源
//客户端
//1、创建客户端Socket,指定服务器地址和端口
Socket socket =newSocket("localhost",10086);
//2、获取输出流,向服务器端发送信息
OutputStream os = socket.getOutputStream();//字节输出流
PrintWriter pw =newPrintWriter(os);//将输出流包装成打印流
pw.write("用户名:admin;密码:123");
pw.flush();
socket.shutdownOutput();
//3、获取输入流,并读取服务器端的响应信息
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String info = null;
while((info=br.readLine())!null){
System.out.println("我是客户端,服务器说:"+info);
}
//4、关闭资源
br.close();
is.close();
pw.close();
os.close();
socket.close();
应用多线程实现服务器与多客户端之间的通信:
① 服务器端创建ServerSocket,循环调用accept()等待客户端连接
② 客户端创建一个socket并请求和服务器端连接
③ 服务器端接受苦读段请求,创建socket与该客户建立专线连接
④ 建立连接的两个socket在一个单独的线程上对话
⑤ 服务器端继续等待新的连接
//服务器线程处理
//和本线程相关的socket
Socket socket =null;
//
public serverThread(Socket socket){
this.socket = socket;
}
publicvoid run(){
//服务器处理代码
}
//============================================
//服务器代码
ServerSocket serverSocket =newServerSocket(10086);
Socket socket =null;
int count =0;//记录客户端的数量
while(true){
socket = serverScoket.accept();
ServerThread serverThread =newServerThread(socket);
serverThread.start();
count++;
System.out.println("客户端连接的数量:"+count);
}
3.2 UDP模式
3.2.1 基本概念
UDP协议(用户数据报协议)是无连接的、不可靠的、无序的,速度快。 进行数据传输时,首先将要传输的数据定义成数据报(Datagram),大小限制在64k,在数据报中指明数据索要达到的Socket(主机地址和端口号),然后再将数据报发送出去
- DatagramPacket类:表示数据报包
- DatagramSocket类:进行端到端通信的类
3.2.2 UDP通信实现步骤
服务器端实现步骤:
① 创建DatagramSocket,指定端口号
② 创建DatagramPacket
③ 接受客户端发送的数据信息
④ 读取数据
//服务器端,实现基于UDP的用户登录
//1、创建服务器端DatagramSocket,指定端口
DatagramSocket socket =new datagramSocket(10010);
//2、创建数据报,用于接受客户端发送的数据
byte[] data =newbyte[1024];//
DatagramPacket packet =newDatagramPacket(data,data.length);
//3、接受客户端发送的数据
socket.receive(packet);//此方法在接受数据报之前会一致阻塞
//4、读取数据
String info =newString(data,o,data.length);
System.out.println("我是服务器,客户端告诉我"+info);
//=========================================================
//向客户端响应数据
//1、定义客户端的地址、端口号、数据
InetAddress address = packet.getAddress();
int port = packet.getPort();
byte[] data2 = "欢迎您!".geyBytes();
//2、创建数据报,包含响应的数据信息
DatagramPacket packet2 = new DatagramPacket(data2,data2.length,address,port);
//3、响应客户端
socket.send(packet2);
//4、关闭资源
socket.close();
客户端实现步骤:
① 定义发送信息
② 创建DatagramPacket,包含将要发送的信息
③ 创建DatagramSocket
④ 发送数据
//客户端
//1、定义服务器的地址、端口号、数据
InetAddress address =InetAddress.getByName("localhost");
int port =10010;
byte[] data ="用户名:admin;密码:123".getBytes();
//2、创建数据报,包含发送的数据信息
DatagramPacket packet = newDatagramPacket(data,data,length,address,port);
//3、创建DatagramSocket对象
DatagramSocket socket =newDatagramSocket();
//4、向服务器发送数据
socket.send(packet);
//接受服务器端响应数据
//======================================
//1、创建数据报,用于接受服务器端响应数据
byte[] data2 = new byte[1024];
DatagramPacket packet2 = new DatagramPacket(data2,data2.length);
//2、接受服务器响应的数据
socket.receive(packet2);
String raply = new String(data2,0,packet2.getLenth());
System.out.println("我是客户端,服务器说:"+reply);
//4、关闭资源
socket.close();
4.关于阻塞
先说说一些比较“轻”的避免手段:
1)发送完后调用Socket的shutdownOutput()方法关闭输出流,这样对端的输入流上的read操作就会返回-1。但是这个方法不能用于通信双方需要多次交互的情况。
2)发送数据时,约定数据的首部固定字节数为数据长度。这样读取到这个长度的数据后,就不继续调用read方法。
3)为了防止read操作造成程序永久挂起,还可以给socket设置超时。
如果read()方法在设置时间内没有读取到数据,就会抛出一个java.net.SocketTimeoutException异常。
例如下面的方法设定超时3秒。
socket.setSoTimeout(3000);
接着我们来看看比较“重”的两种解决方案:
4.1 多线程解决堵塞
主要是为了解决多个客户端同时访问引起的阻塞问题,上面已经谈到了多线程的实现,不过可以更进一步,在服务器端加上线程池,使处理更加高效:
服务器源码:
public class Server {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(9527), 10);
final AtomicInteger count = new AtomicInteger(0);
ExecutorService pool = Executors.newCachedThreadPool(new ThreadFactory() {
public Thread newThread(Runnable r) {
return new Thread(r, "ThreadPool-new-" + count.incrementAndGet());
}
});
while(true) {
try {
final Socket clientSocket = serverSocket.accept();
pool.execute(new Runnable() {
public void run() {
String client = clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort();
System.out.println(client + " received!");
System.out.println(Thread.currentThread().getName() + " handle " + client);
try {
clientSocket.setTcpNoDelay(true);
clientSocket.setReuseAddress(true);
BufferedReader bufReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream());
String line = bufReader.readLine();
System.out.println(client + " say:" + line);
writer.println("received '" + line + "'");
writer.flush();
Thread.sleep(10000);
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
4.2 使用NIO非阻塞通信
NIO则具有非阻塞的特性,可以通过对channel的阻塞行为的配置,实现非阻塞式的信道。在非阻塞情况下,线程在等待连接,写数据等(标准IO中的阻塞操作)的同时,也可以做其他事情,这便实现了线程的异步操作。
这里有关于nio的详细说明:
https://www.jianshu.com/p/052035037297
-
非阻塞式网络IO的特点:
1)把整个过程切换成小的任务,通过任务间协作完成。
2)由一个专门的线程来处理所有的 IO 事件,并负责分发。
3)事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
4)线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的进程切换
5)Java NIO引入了选择器的概念,选择器可以监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道,这也是非阻塞IO的核心。而在标准IO的Socket编程中,单个线程则只能在一个端口监听。
参考:
-
NIO模式的基本原理描述如下:
服务端打开一个通道(ServerSocketChannel),并向通道中注册一个选择器(Selector),这个选择器是与一些感兴趣的操作的标识(SelectionKey,即通过这个标识可以定位到具体的操作,从而进行响应的处理)相关联的,然后基于选择器(Selector)轮询通道(ServerSocketChannel)上注册的事件,并进行相应的处理。
客户端在请求与服务端通信时,也可以像服务器端一样注册(比服务端少了一个SelectionKey.OP_ACCEPT操作集合),并通过轮询来处理指定的事件,而不必阻塞。
服务器端源码示例:
public class NIOServer {
private static int BUFF_SIZE=1024;
private static int TIME_OUT = 2000;
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(10083));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
TCPProtocol protocol = new EchoSelectorProtocol(BUFF_SIZE);
while (true) {
if(selector.select(TIME_OUT)==0){
//在等待信道准备的同时,也可以异步地执行其他任务, 这里打印*
System.out.print("*");
continue;
}
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
while (keyIter.hasNext()) {
SelectionKey key = keyIter.next();
//如果服务端信道感兴趣的I/O操作为accept
if (key.isAcceptable()){
protocol.handleAccept(key);
}
//如果客户端信道感兴趣的I/O操作为read
if (key.isReadable()){
protocol.handleRead(key);
}
//如果该键值有效,并且其对应的客户端信道感兴趣的I/O操作为write
if (key.isValid() && key.isWritable()) {
protocol.handleWrite(key);
}
//这里需要手动从键集中移除当前的key
keyIter.remove();
}
}
}
}
public interface TCPProtocol {
void handleAccept(SelectionKey key) throws IOException;
void handleRead(SelectionKey key) throws IOException;
void handleWrite(SelectionKey key) throws IOException;
}
public class EchoSelectorProtocol implements TCPProtocol {
private int bufSize; // 缓冲区的长度
public EchoSelectorProtocol(int bufSize){
this.bufSize = bufSize;
}
@Override
public void handleAccept(SelectionKey key) throws IOException {
System.out.println("Accept");
SocketChannel socketChannel = ((ServerSocketChannel)key.channel()).accept();
socketChannel.configureBlocking(false);
socketChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));
}
@Override
public void handleRead(SelectionKey key) throws IOException {
SocketChannel clntChan = (SocketChannel) key.channel();
//获取该信道所关联的附件,这里为缓冲区
ByteBuffer buf = (ByteBuffer) key.attachment();
buf.clear();
long bytesRead = clntChan.read(buf);
//如果read()方法返回-1,说明客户端关闭了连接,那么客户端已经接收到了与自己发送字节数相等的数据,可以安全地关闭
if (bytesRead == -1){
clntChan.close();
}else if(bytesRead > 0){
//如果缓冲区总读入了数据,则将该信道感兴趣的操作设置为为可读可写
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
}
@Override
public void handleWrite(SelectionKey key) throws IOException {
// TODO Auto-generated method stub
ByteBuffer buffer=(ByteBuffer) key.attachment();
buffer.flip();
SocketChannel clntChan = (SocketChannel) key.channel();
//将数据写入到信道中
clntChan.write(buffer);
if (!buffer.hasRemaining()){
//如果缓冲区中的数据已经全部写入了信道,则将该信道感兴趣的操作设置为可读
key.interestOps(SelectionKey.OP_READ);
}
//为读入更多的数据腾出空间
buffer.compact();
}
}
客户端:
public class NIOClient {
public static void main(String[] args) throws IOException {
SocketChannel clntChan = SocketChannel.open();
clntChan.configureBlocking(false);
if (!clntChan.connect(new InetSocketAddress("localhost", 10083))){
//不断地轮询连接状态,直到完成连接
while (!clntChan.finishConnect()){
//在等待连接的时间里,可以执行其他任务,以充分发挥非阻塞IO的异步特性
//这里为了演示该方法的使用,只是一直打印"."
System.out.print(".");
}
}
//为了与后面打印的"."区别开来,这里输出换行符
System.out.print("\n");
//分别实例化用来读写的缓冲区
ByteBuffer writeBuf = ByteBuffer.wrap("send send send".getBytes());
ByteBuffer readBuf = ByteBuffer.allocate("send".getBytes().length-1);
while (writeBuf.hasRemaining()) {
//如果用来向通道中写数据的缓冲区中还有剩余的字节,则继续将数据写入信道
clntChan.write(writeBuf);
}
StringBuffer stringBuffer=new StringBuffer();
//如果read()接收到-1,表明服务端关闭,抛出异常
while ((clntChan.read(readBuf)) >0){
readBuf.flip();
stringBuffer.append(new String(readBuf.array(),0,readBuf.limit()));
readBuf.clear();
}
//打印出接收到的数据
System.out.println("Client Received: " + stringBuffer.toString());
//关闭信道
clntChan.close();
}
}
参考引用: