一 什么是Socket
Socket的概念很简单,它是网络上运行的两个程序间双向通讯的一端,既可以接收请求,也可以发送请求,利用它可以较为方便地编写网络上数据的传递。
所以简而言之,Socket就是进程通信的端点,Socket之间的连接过程可以分为几步:
1、服务器监听
服务器端Socket并不定位具体的客户端Socket,而是处于等待连接的状态,实时监控网络状态
2、客户端请求
客户端Socket发出连接请求,要连接的目标是服务端Socket。为此,客户端Socket必须首先描述它要连接的服务端Socket,指出服务端Socket的地址和端口号,然后就向服务端Socket提出连接请求
3、连接确认
当服务端Socket监听到或者说是接收到客户端Socket的连接请求,它就响 应客户端Socket的请求,建立一个新的线程,把服务端Socket的描述发给客户端,一旦客户端确认了此描述,连接就好了。而服务端Socket继续 处于监听状态,继续接收其他客户端套接字的连接请求
二 Socket的两种模式
Socket有两种主要的操作方式:面向连接和无连接的。
2.1 面向连接的Socket
操作就像一部电话,必须建立一个连接和一人呼叫,所有事情在达到时的顺序与它们出发时的顺序一样,但可靠性,正确性和有序性有所保障
面向连接的操作使用TCP协议。一个这个模式下的Socket必须在发送数据之前 与目的地的Socket取得一个连接,一旦连接建立了,Socket就可以使用一个流接口:打开-->读-->写-->关闭,所有发送 的信息都会在另一端以同样的顺序被接收。面向连接的操作比无连接的操作效率更低,但是数据的安全性更高。
2.2 无连接的Socket
操作就像是一个邮件投递,没有什么 保证,多个邮件可能在达到时的顺序与出发时的顺序不一样。快速、高效,但是数据安全性不佳。
无连接的操作使用数据报协议。一个数据报是一个独立的单元,它包含了所有这次投递的信息,就像一个信封,它有目的地址和要发送的内容,这个模式下的Socket并不需要连接一个目的Socket,它只是简单地透出数据报,无连接的操作是快速、高效的,但是数据安全性不佳。
到底使用哪种模式是由应用程序的需要决定的。如果可靠性更重要的话,用面向连接的 操作会好一些,比如文件服务器需要数据的正确性和有序性,如果一些数据丢失了,系统的有效性将会失去;比如一些服务器间歇性地发送一些数据块,如果数据丢 失了的话,服务器并不想要再重新发送一次,因为当数据到达的时候,它可能已经过时了。确保数据的有序性和正确性需要额外的操作的内存消耗,额外的消耗将会 降低系统的回应速率。
三 利用Java开发Socket
在Java中面向连接的类有两种形式,它们分别是客户端和服务器端,先看一下
3.1 简化版
服务器端:
public class HelloServer
{
public static void main(String[] args) throws IOException
{
ServerSocket serverSocket = null;
try
{
// 实例化一个服务器端的Socket连接
serverSocket = new ServerSocket(9999);
}
catch (IOException e)
{
System.err.print("Could not listen on port:9999");
System.exit(1);
}
Socket clientSocket = null;
try
{
// 用于接收来自客户端的连接
clientSocket = serverSocket.accept();
}
catch (IOException e)
{
System.err.println("Accept failed");
System.exit(1);
}
// 客户端有数据了就向屏幕打印Hello World
System.out.print("Hello World");
clientSocket.close();
serverSocket.close();
}
}
此代码的作用就是构造出服务端Socket,并等待来自客户端的消息。当然,此时运行代码是没有任何反应的,因为服务端在等待客户端的连接。
客户端:
public class HelloClient
{
public static void main(String[] args) throws IOException
{
Socket socket = null;
BufferedReader br = null;
// 下面这段程序,用于将输入输出流和Socket相关联
try
{
socket = new Socket("localhost", 9999);
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
catch (UnknownHostException e)
{
System.err.println("Don't know about host:localhost");
System.exit(1);
}
catch (IOException e)
{
System.err.println("Could not get I/O for the connection");
System.exit(1);
}
System.out.print(br.readLine());
br.close();
socket.close();
}
}
此时只需要先运行HelloServer,再运行HelloClient,保证服务器先监听,客户端后发送,就可以在控制台上看到"Hello World"了。
3.2 改进版本的Socket
上面的Socket演示的效果是,服务器端Socket收到了来自客户端Socket的数据,但是并没有真正地体现服务器端Socket和客户端Socket的交互,下面演示一下利用Socket进行服务器端和客户端的交互
服务器端
public class EchoServer
{
public static void main(String[] args) throws IOException
{
ServerSocket ss = null;
PrintWriter pw = null;
BufferedReader br = null;
try
{
// 实例化监听端口
ss = new ServerSocket(1111);
}
catch (IOException e)
{
System.err.println("Could not listen on port:1111");
System.exit(1);
}
Socket incoming = null;
while (true)
{
incoming = ss.accept();
pw = new PrintWriter(incoming.getOutputStream(), true);
// 先将字节流通过InputStreamReader转换为字符流,之后将字符流放入缓冲之中
br = new BufferedReader(new InputStreamReader(incoming.getInputStream()));
// 提示信息
pw.println("Hello!...");
pw.println("Enter BYE to exit");
pw.flush();
// 没有异常则不断循环
while (true)
{
// 只有当用户输入时才返回数据
String str = br.readLine();
// 当用户连接断掉时会返回空值null
if (str == null)
{
// 退出循环
break;
}
else
{
// 对用户输入字符串加前缀Echo并将此信息打印到客户端
pw.println("Echo:" + str);
pw.flush();
// 退出命令,equalsIgnoreCase()是不区分大小写的
if ("BYE".equalsIgnoreCase(str.trim()))
{
break;
}
}
}
// 该close的资源都close掉
pw.close();
br.close();
incoming.close();
ss.close();
}
}
}
客户端
public class EchoClient
{
public static void main(String[] args) throws IOException
{
Socket socket = null;
PrintWriter pw = null;
BufferedReader br = null;
try
{
socket = new Socket("localhost", 1111);
pw = new PrintWriter(socket.getOutputStream(), true);
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
catch (UnknownHostException e)
{
System.err.println("Don't know abount host:localhost");
System.exit(1);
}
System.out.println(br.readLine());
System.out.println(br.readLine());
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
String userInput;
// 将客户端Socket输入流(即服务器端Socket的输出流)输出到标准输出上
while ((userInput = stdIn.readLine()) != null)6 {
pw.println(userInput);
System.out.println(br.readLine());
}
// 同样的,将该关闭的资源给关闭掉
pw.close();
br.close();
socket.close();
}
}
3.3 服务端多监听
程序写到上面,已经基本成型了,不过还有一个问题:现实情况中,一个服务器端的Socket不可能只对应一个客户端的Socket,必然一个服务器端的Socket可以接收来自多个客户端的Socket的请求。
解决上述问题的办法就是多线程。大致代码是这样的:
public class HandleThread extends Thread
{
private Socket socket;
public HandleThread(Socket socket)
{
this.socket = socket;
}
public void run()
{
// Socket处理代码
}
}
public static void main(String[] args) throws IOException
{
ServerSocket serverSocket = null;
try
{
// 实例化一个服务器端的Socket连接
serverSocket = new ServerSocket(9999);
}
catch (IOException e)
{
System.err.print("Could not listen on port:9999");
System.exit(1);
}
Socket clientSocket = null;
try
{
while (true)
{
// 用于接收来自客户端的连接
clientSocket = serverSocket.accept();
new HandleThread(clientSocket).start();
}
}
catch (IOException e)
{
System.err.println("Accept failed");
System.exit(1);
}
}
即,服务器端启动一个永远运行的线程,监听来自客户端的Socket,一旦客户端有Socket到来,即开启一个新的线程将Socket交给线程处理。