Socket和ServerSocket是网络编程非常重要的两个类,前者用于建立客户端套接字,并读写服务器;后者用于创建服务端套接字,监听某个端口并处理到达的客户端请求。按照TCP/IP的四层参考模型 从上到下依次是应用层、传输层、网络层、物理层,而Socket是工作与应用层和传输层之间,它抽象了应用层以下的通信复杂细节,从设计模式的角度将Socket就是一个门面模式。
Socket网络编程中对于信道两端流的操作是至关重要的,这一块还是有很多需要注意的地方,当然Socket和ServerSocket对于流的操作都是阻塞式的,下一篇博文我会展示使用NIO模型编写的网络通信示例,当我在初学Socket网络编程时在这一块遇到过这样的问题,我用如下的代码读取socket数据:
InputStreamReader reader = new InputStreamReader(socket.getInputStream());
StringBuilder readContent = new StringBuilder();
for(int c = reader.read(); c!=-1; c= reader.read()){
readContent.append((char)c);
}
System.out.println("readContent: "+ readContent);
但是始终得不到输出,后来debug代码总是阻塞在read方法上,我们知道read()返回一个字节,整数值在0-255之间,当没有数据读取时会一直阻塞,当读到流的末尾时会返回-1,但是socket流和文件流又有不同,read方法读到文件的末尾就是流的末尾为返回-1,但是socket保持连接期间流一直没有结束,信道的另一端随时都有可能发送数据过来,这样也就不会返回-1,所以如果没有数据读取read方法会一直阻塞等待对端的数据到达。为了解决这个问题我们就需要人为地告诉读取数的一方什么时候跳出。
常用的解决方法:
(1)自定义一个读写消息的简单协议用于约束读取数据的方式,比如在数据流的首字节代表需要读取的字节数量,读到这个数量的字节就不再读
(2)一端发送完数据之后调用Socket的shutdownOutput()方法,这样对端再读取数据时就会返回-1,当然这只是调整与socket关联的流,并没有关闭socket。
(3)客户端和服务端约定读取结束的标识符
(4)设置socket读取的超时时间,超过这个时间socket抛出异常
这篇博客不讲述网络编程中各个类的使用方式,这些在网上都有很多教程了,这里我提供一个常用的网络编程客户端与服务端的通信代码示例,示例中会详细讲解每句代码的含义。
如下是客户端的代码示例:
/**
* @author yujie.wang
* 客户端Socket,用于与服务端ServerSocket建立Tcp连接通信
*/
public class Socket_Client {
private static final String DEFAULT_SERVER = "127.0.0.1";
private static final int DEFAULT_PORT = 4568;
private static final int DEFAULT_TIME_OUT = 5000;
private Socket socket;
public Socket_Client(){
this(DEFAULT_SERVER,DEFAULT_PORT,DEFAULT_TIME_OUT);
}
public Socket_Client(String host, int port, int timeout){
initSocket(host, port,timeout);
}
/**
* 初始化客户端Socket
* @param host
* @param port
*/
public void initSocket(String host, int port, int timeout){
try {
//建立与主机host在端口port的连接,这个过程会阻塞,直到连接建立
//如果连接无法建立 则会抛出相应的异常
socket = new Socket(host,port);
//设置socket读取数据的超时时间
socket.setSoTimeout(5000);
//设置小数据包不再组合成大包发送,也不再等待前一个数据包返回确认消息
socket.setTcpNoDelay(true);
//设置如果客户端Socket关闭了,未发送的包直接丢弃
socket.setSoLinger(true, 0);
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 判断客户端Socket连接状态是否正打开
* @return
*/
public boolean isOpen(){
//只要Socket与服务器曾经建立过连接isConnected()就会返回真,不管当前socket是否已经断开连接
//socket关闭了或者 socket从来没有与服务器建立过连接 都会返回false
if(socket.isConnected() && !socket.isClosed()){
// socket 正在保持连接 可以正常使用
return true;
}
return false;
}
public void close(){
if(socket != null){
try {
socket.close();
System.out.println("client socket close");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void task(String data){
if(isOpen()){
try {
//根据制定的简单协议读写数据
Protocol.write(socket.getOutputStream(),data);
Protocol.read(socket.getInputStream());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
close();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("task begin");
Socket_Client test = new Socket_Client();
test.task("Hello World !");
System.out.println("task end");
}
}
/**
* @author yujie.wang
* 服务端ServerSocket
*/
public class ServerSocket_Server {
// 创建处理请求任务的线程池
private ExecutorService pool = Executors.newFixedThreadPool(5);
private ServerSocket server;
private final static int DEFAULT_PORT = 4568;
public ServerSocket_Server(){
this(DEFAULT_PORT);
}
public ServerSocket_Server(int port){
initServerSocket(port);
}
/**
* 初始化创建服务端socket
* @param port
*/
public void initServerSocket(int port){
if(port <= 0){
port = DEFAULT_PORT;
}
try {
//服务端socket监听port
server = new ServerSocket(port);
System.out.println("server listening on port: "+ port);
} catch (IOException e) {
// TODO Auto-generated catch block
System.out.println("server init failed"+ e);
}
}
/**
* 关闭服务端socket
* @param server
*/
public void close(){
if(server != null){
try {
server.close();
System.out.println("关闭ServerSocket");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void serverStart(){
while(true){
try {
//accept()方法会阻塞,直到有连接到达服务端socket监听的端口
//当客户端请求达到时accept()方法返回一个客户端Socket对象
//实际上这个过程是 服务端socket从操作系统内核的客户端请求队列中 将客户端请求socket取出
Socket clientSocket = server.accept();
System.out.println("server accpet a request from host: "
+ clientSocket.getInetAddress().getHostAddress()
+ " on port: "+ clientSocket.getPort());
//这里使用线程池来处理具体的任务
executeTask task = new executeTask(clientSocket);
pool.submit(task);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
close();
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int port = DEFAULT_PORT;
if(args.length != 0){
port = Integer.valueOf(args[0]);
}
ServerSocket_Server server = new ServerSocket_Server(port);
server.serverStart();
}
private static class executeTask implements Callable<Void>{
private Socket socket;
public executeTask(Socket socket){
this.socket = socket;
}
@Override
public Void call() throws Exception {
// TODO Auto-generated method stub
try {
// 从客户端socket获取输入流 读取客户端参数
String clientIndex = Protocol.read(socket.getInputStream());
System.out.println("server_client read content: "+ clientIndex);
String index = clientIndex +" Red Apple";
Protocol.write(socket.getOutputStream(),index);
} catch (Exception e) {
// TODO: handle exception
System.out.println(e);
}finally{
if(socket != null){
try {
socket.close();
} catch (Exception e2) {
// TODO: handle exception
System.out.println("close socket exception: "+ e2);
}
}
}
return null;
}
}
}
其中我们制定了一个简单的数据读写协议用于处理流的读写。
/**
*
* @author yujie.wang
* 这是一个简单的对流读写的协议,只读取流中的前5个字节就跳出。
* 当然这个协议主要是解决read()方法读取socket流阻塞的问题
*/
public class Protocol {
public static String read(InputStream in){
try {
InputStreamReader reader = new InputStreamReader(in);
StringBuilder readContent = new StringBuilder();
// read()读取不到数据会阻塞,或者当读到流的末尾会返回-1
// 但是socket流和文件流又有不同,read方法读到文件的末尾就是流的末尾为返回-1
// 但是socket流没有结束标记位,所以如果没有数据读取read方法会一直阻塞
// 所以这里模拟了一个数据包 每个包数据大小就是5个字节
int count = 0;
for(int c = reader.read(); c!=-1; c= reader.read()){
readContent.append((char)c);
//System.out.println("readContent.toString():" +readContent.toString());
if(count++ >= 5)
break;
}
System.out.println("readContent.toString():" +readContent.toString());
return readContent.toString();
//只关闭输入流,关闭之后再读返回-1
//socket.shutdownInput();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return "0";
}
public static void write(OutputStream outStream, String data){
DataOutputStream out;
try {
out = new DataOutputStream(outStream);
out.write(data.getBytes());
out.flush();
//只关闭输出流,关闭之后再写 会抛出一个IOException。
//socket.shutdownOutput();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
依次启动服务端和客户端代码,程序输出如下:
server listening on port: 4568
server accpet a request from host: 127.0.0.1 on port: 52663
readContent.toString():Hello
server_client read content: Hello
task begin
readContent.toString():Hello
client socket close
task end