参考资料:https://www.cnblogs.com/goodboy-heyang/p/6372058.html
Socket通信实现步骤:1、创建ServerSocket和Socket(client)。2、打开连接到Socket的输入/输出流。3、按照协议对Socket进行读/写操作。4、关闭输入输出流、关闭Socket。
一定有两个不同的Socket(要么ip不同,要么port不同)才能通信!!!
1、创建ServerSocket对象,绑定监听端口。 new ServerSocket(8888)
2、通过accept()方法监听客户端请求。 Socket socket = serverSocket.accept()
3、链接建立后,通过输入流读取客户端发送的请求信息。 socket.getInputStream();
4、通过输出流向客户端发送响应信息。 socket.getOutputStream()
5、关闭相关资源。
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
while (true)
{
// 阻塞 直到有socket连接serverSocket
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();// 得到一个输入流,接收客户端传递的信息
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);// 提高效率,将自己字节流转为字符流
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);// 加入缓冲区
CharBuffer charBuffer = CharBuffer.allocate(1024);
while (bufferedReader.read(charBuffer) != -1) {
charBuffer.flip();
char[] inc = new char[charBuffer.limit()];
System.out.println("已接收到客户端连接");
charBuffer.get(inc);
System.out.println(Thread.currentThread().getName() + "服务端接收到客户端信息:" + new String(inc)
+ ",当前客户端ip为:" + socket.getInetAddress().getHostAddress());
charBuffer.clear();
}
}
}
}
1、创建Socket对象,指明需要连接的服务器的地址和端口号。 new Socket(“localhost”,8888) 相当于connect localhost:8888 但自身的socket对应端口肯定不是8888,由socket.getLocalPort()获取,随机产生(如62572)
2、连接建立后,通过输出流向服务器端发送请求信息。 socket.getOutputStream()
3、通过输入流获取服务器响应的信息。 socket.getInputStream()
4、关闭相关资源。
public class Client {
public static void main(String[] args) {
// 1、创建客户端Socket,指定服务器地址和端口
try {
Socket socket = new Socket("localhost", 8888);
// 2、获取输出流,向服务器端发送信息
OutputStream os = socket.getOutputStream();// 获取字节输出流
// 将输出流包装为打印流
PrintWriter pw = new PrintWriter(os);
pw.write("用户名:admin 密码:123");
pw.flush();
while(true){
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
CharBuffer cb = CharBuffer.allocate(1024);
int rs = br.read(cb);
if(rs>0){
cb.flip();
char[] c = new char[cb.limit()];
cb.get(c);
System.out.println("接收来自服务端的消息:"+new String(c));
}
}
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
参考资料:https://blog.csdn.net/yinwenjie/article/details/48472237
阻塞模型
一定要认识到,上述socket通信是标准的阻塞模型(BIO),具体反映在serverSocket.accept(),serverSocket接收到新连接前应用程序一直阻塞,当操作系统接到客户端A的连接请求后,才会建立socket连接,服务端接收传递数据,且在处理数据过程中,客户端B的请求连接是一直阻塞的。
进程中只有一个线程,只能同时处理一个socket!!!
让我们来看下socket.accept()源码,位于DualStackPlainSocketImpl中。默认情况下,是没有设置timeout的,所以timeout=0,直接进入connect0方法。这样就会无限期阻塞等待,除非有连接进入。
void socketConnect(InetAddress address, int port, int timeout)
throws IOException {
int nativefd = checkAndReturnNativeFD();
if (address == null)
throw new NullPointerException("inet address argument is null.");
int connectResult;
// 默认情况下timeout并未设置
if (timeout <= 0) {
connectResult = connect0(nativefd, address, port);
} else {
configureBlocking(nativefd, false);
try {
connectResult = connect0(nativefd, address, port);
if (connectResult == WOULDBLOCK) {
waitForConnect(nativefd, timeout);
}
} finally {
configureBlocking(nativefd, true);
}
}
/*
* We need to set the local port field. If bind was called
* previous to the connect (by the client) then localport field
* will already be set.
*/
if (localport == 0)
localport = localPort0(nativefd);
}
非阻塞模式
为了避免serverSocket.accept()没有设置timeout而造成的无限期阻塞,可以认为设置timeout值,这样如果在时间内仍没有新连接产生,则抛出SocketTimeoutException。这样应用程序就可以在阻塞间隔做其他事,过段时间再来访问。
通过serverSocket.setSoTimeout()来设置accept阻塞时间,
通过socket.setSoTimeout()来设置read超时时间。
其实这种隔段时间再来访问操作系统的模式也可以称之为轮询
调用serverSocket.setSoTimeout()方法设置accept失效时间
while (true)
{
serverSocket.setSoTimeout(1000);
Socket socket = null;
try{
socket = serverSocket.accept();
}catch(Exception e){
//===========================================================
// 执行到这里,说明本次accept没有接收到任何TCP连接
// 主线程在这里就可以做一些事情,记为X
//===========================================================
synchronized (Server.xWait) {
System.out.println("socket 还未接收到消息");
}
continue;
}
...
}
调用socket.setSoTimeout()设置read失效时间
//下面我们收取信息(设置成非阻塞方式,这样read信息的时候,又可以做一些其他事情)
socket.setSoTimeout(10);
BIORead:while(true) {
try {
while((realLen = in.read(contextBytes, 0, maxLen)) != -1) {
message.append(new String(contextBytes , 0 , realLen));
/*
* 我们假设读取到“over”关键字,
* 表示客户端的所有信息在经过若干次传送后,完成
* */
if(message.indexOf("over") != -1) {
break BIORead;
}
}
} catch(SocketTimeoutException e2) {
//===========================================================
// 执行到这里,说明本次read没有接收到任何数据流
// 主线程在这里又可以做一些事情,记为Y
//===========================================================
SocketServer3.LOGGER.info("这次没有从底层接收到任务数据报文,等待10毫秒,模拟事件Y的处理时间");
continue;
}
}
添加多线程
当有多个客户端同时连接服务端时,上述的非阻塞方法也无法解决高并发问题,因为socket在处理通信数据过程中,客户端是阻塞的。为了处理多个客户端请求,服务端可采用多线程方法,在操作系统监听到新的连接后,新建一个线程去处理这个连接的通信,这样就不会造成客户端A连接服务端时,客户端B却一直阻塞没法与服务端通信。
现在一个进程有多个线程,对应多个socket
while(true) {
Socket socket = null;
try {
socket = serverSocket.accept();
} catch(SocketTimeoutException e1) {
//===========================================================
// 执行到这里,说明本次accept没有接收到任何TCP连接
// 主线程在这里就可以做一些事情,记为X
//===========================================================
synchronized (SocketServer4.xWait) {
SocketServer4.LOGGER.info("这次没有从底层接收到任何TCP连接,等待10毫秒,模拟事件X的处理时间");
SocketServer4.xWait.wait(10);
}
continue;
}
//当然业务处理过程可以交给一个线程(这里可以使用线程池),并且线程的创建是很耗资源的。
//最终改变不了.accept()只能一个一个接受socket连接的情况
SocketServerThread socketServerThread = new SocketServerThread(socket);
new Thread(socketServerThread).start();
}
但要知道最大线程数是和cpu操作系统息息相关的,且每创建一个线程都得需要serverSocket.accept()获取socket,accept()只能一个个接收socket连接,相当于你又一个很大的水管,连接的水龙头却是在慢慢滴水。更重要的是需要明白,accept()阻塞的原因是调用的操作系统模型是同步IO,应用程序可以是阻塞/非阻塞。
总结
这次介绍的socket io 采用的模型是 阻塞同步 和 非阻塞同步
阻塞/非阻塞是对于应用程序而言,应用程序在等待操作系统接收新连接的时候,可以无限期阻塞,也可以设定timeout时间,超时就做其他事,过会再来查看是否有新连接。
同步/异步是对于操作系统而言,同步是指应用程序和操作系统必须一问一答,类似于http请求,操作系统是不会主动告知应用程序是否有新的连接进入;异步是操作系统接收到应用程序请求后,不会立刻回答结果,等有连接产生了才会通知应用程序,类似于ajax请求和订阅通知。
至于java nio框架,采用的selector channel buffer三大要素,通过selector来监控所有事件,并通知ServerSocketChannel和SocketChannel,称之为多路复用IO
java aio,则是真正意义上的异步IO