Socket通信详解
阅读原文时间:2021年04月20日阅读:1

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

建立一个完整的socket(套接字),需要调用java.net包中的Socket类和ServerSocket类(Socket和ServerSocket的工作都是通过SocketImpl类及其子类完成的),客户端的输出流是服务器端的输入流,服务器端的输出流是客户端的输入流。

首先用SQLyog创建一个数据库socket,需要记住用户名root密码1234,创建user、file表。

到时候使用获取mysql链接时方便进行数据库操作。

第一部分:服务器监听

创建Server类,服务器端必须先创建ServerSocket对象,并使用accept()方法监听等待客户端。

Java API为TCP协议提供的类分为Socket和ServerSocket,可以简单的理解为客户端Socket和服务端Socket,更具体的讲Socket对象是主动发起连接请求的,而ServerSocket对象是等待接收连接请求的。

代码:

package com.socket;



import java.io.IOException;

import java.net.ServerSocket;

import java.net.Socket;



/*
 * 服务器端主程序操作
 */
public class Server {
    public static void main(String[] args) {
        try {
            //1.创建服务器端的ServerSocket,指定伴随的端口号
            ServerSocket serversocket=new ServerSocket(8881);
            System.out.println("****服务器端即将启动,等待客户端*****");// 提示服务器开启
            Socket socket=null;
            while(true){
                //2.调用accept()方法开始监听,等待客户端连接
                socket=serversocket.accept();
                //调用线程
                ServerThread st=new ServerThread(socket);
                st.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

等待客户端连接需要创建线程ServerThread,并开启线程。

代码:

package com.socket;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.sql.SQLException;
//这里是根据客户端的需求写的响应
import com.Server_To_Client_Response.Add_File_Response;
import com.Server_To_Client_Response.Add_User_Response;
import com.Server_To_Client_Response.Select_User_Response;
public class ServerThread extends Thread {
    //创建与本线程相关的socket
     Socket socket=null;
     ObjectInputStream ois=null;
     //重写构造函数
     public ServerThread(Socket socket){
        this.socket=socket; 
     }
     //线程操作 获取输出流,响应客户端的请求
     public void run(){
            try {
                //3.获取输入流,并读取客户端信息
                ois=new ObjectInputStream(socket.getInputStream());
                     /*
                      * 读取接收到的对象信息
                      */
                //available() 方法返回从这个输入流中读取余下的字节数
                     while( ois.available()!=-1){
                     //输入流读取的信息对象化
                     ClientToServerInfo ctsi =(ClientToServerInfo)ois.readObject();
                     System.out.println(ctsi.toString());//打印客户端传到服务器的数据(对象)
                     /*
                      * 需要客户端传入的对象数据进行分析,根据传入的sign标志值进行相应的操作
                      * sign="1"完成注册会员操作,链接数据库,向数据库中user表添加记录
                      * sign="2"完成会员登录操作,连接数据库,在数据库中user表查询记录
                      * sign="3"完成文件上传操作,连接数据库,向数据库中file表添加记录
                      */
                     //System.out.println(ctsi.sign);
                     if(ctsi.sign.equals("1")){
                         //调用 Add_User_Response类的构造方法对客户端传入的信息进行详细的响应
                         Add_User_Response aur=new Add_User_Response(socket, ctsi.name,ctsi.password_OR_path);
                        //System.out.println("运行完Add_User_Response类");
                        break;
                     }
                     if(ctsi.sign.equals("2")){
                         //调用 Select_User_Response类的构造方法对客户端传入的信息进行详细的响应
                         Select_User_Response sur=new Select_User_Response(socket, ctsi.name,ctsi.password_OR_path);
                         //System.out.println("运行完 Select_User_Response类");
                         break;
                     }
                     if(ctsi.sign.equals("3")){
                        //调用  Add_File_Response类的构造方法对客户端传入的信息进行详细的响应
                         Add_File_Response afr=new Add_File_Response(socket, ctsi.name,ctsi.password_OR_path);
                        // System.out.println("运行完 Add_File_Response类");
                         break;
                     }
                     }
                    //5.关闭资源
                     ois.close();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

     }

创建传输对象,必须进行序列化:

/*
 * 创建存储需要传输信息的对象,方便客户端向服务器端传送数据
 */

package com.socket;
import java.io.Serializable;
public class ClientToServerInfo implements Serializable{
//当我们需要把对象的状态信息通过网络进行传输,或者需要将对象的状态信息持久化,以便将来使用时都需要把对象进行序列化
    String sign;
    String name;
    String password_OR_path;
    //重写构造函数
    public ClientToServerInfo(String sign,String name,String password_OR_path){
        this.sign=sign;
        this.name=name;
        this.password_OR_path=password_OR_path;
    }
    
    public String getSign() {
        return sign;
    }
    public void setSign(String sign) {
        this.sign = sign;
    }
    public String getName() {
        return name;
    }
    public void setName(String Name) {
        this.name = name;
    }
    public String getPassword_OR_path() {
        return password_OR_path;
    }
    public void setPassword_OR_path(String password_OR_path) {
        this.password_OR_path = password_OR_path;
    }
    @Override
    public String toString() {
        return "客户端说: [sign=" + sign + ", name=" + name + ", password_OR_path=" +password_OR_path + "]";
    }

}

上传文件响应:

代码:

package com.Server_To_Client_Response;
/*
 * 判断客户端传入到服务器端的上传文件数据,提交到数据库中,
 * 是否在数据库中添加成功,并完成对客户端的响应,成功返回上传成功,
 * 反之提示其重新上传
 */
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.sql.SQLException;


import com.database.Database_Add_File;


public class Add_File_Response {
    OutputStream os=null;
    PrintWriter pw=null;
    Socket socket=null;
    boolean flag;
    public Add_File_Response(Socket socket,String name,
            String password_OR_path ) throws SQLException, IOException{
        //将已创建的socket对象加载进来
        this.socket=socket;
        os=socket.getOutputStream();
        pw=new PrintWriter(os);
        /*
         * 创建和数据库的链接,完成向file表格中添加记录操作
         *  添加成功返回false,否则返回true
         */
        
        Database_Add_File dau=new Database_Add_File();
        flag=dau.Database_Add_File_run(name, password_OR_path);
        
        /*
         * 对插入数据后的返回值进行分析,并对客户端进行响应
         */
        if(!flag){
            //获取输出流,响应客户端的请求
                pw.write("文件上传成功!");
                pw.flush();
        }
        else{
            pw.write("文件上传失败!请重新上传!");
            pw.flush();
        }
        //关闭输入输出流(关闭资源)
        pw.close();
        os.close();
    }
}

注册用户响应:

package com.Server_To_Client_Response;


/*
 * 判断客户端传入到服务器端的注册会员数据,提交到数据库中,
 * 是否在数据库中添加成功,并完成对客户端的响应,成功返回注册成功,
 * 反之提示其重新注册
 */
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.sql.SQLException;


import com.database.Database_Add_User;


public class Add_User_Response {
    Socket socket=null;
    boolean flag;
    public Add_User_Response(Socket socket,String name,
            String password_OR_path ) throws SQLException{
        
        try {
            System.out.println("运行Add_User_Response类");
            //将已创建的socket对象加载进来
            
            this.socket=socket;
            OutputStream os=socket.getOutputStream();
            // 根据现有的 OutputStream 创建不带自动行刷新的新 PrintWriter
            PrintWriter pw=new PrintWriter(os);
            /*
             * 创建和数据库的链接,完成向user表格中添加记录操作
             * 添加成功返回false,否则返回true
             */
            Database_Add_User dau=new Database_Add_User();
            flag=dau.Database_Add_User_run(name, password_OR_path);
            //System.out.println("运行完Database_Add_User类");
            /*
             * 对插入数据后的返回值进行分析,并对客户端进行响应
             */
            if(!flag){
                //获取输出流,响应客户端的请求
                    pw.write("您已经成功注册!");
                    pw.flush();
            }
            else{
                pw.write("注册失败!");
                pw.flush();
            }
            //关闭输入输出流(关闭资源)
            pw.close();
            os.close();
            } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally {
            try {
                socket.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        }

    }

查找用户响应:

package com.Server_To_Client_Response;


/*
 * 判断客户端传入到服务器端的会员数据,提交到数据库中,
 * 在数据库中查找,是否注册,密码是否正确,若账号名和密码都正确,登陆成功
 * 并完成对客户端的响应。
 */
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.sql.SQLException;


import com.database.Database_Select_User;


public class Select_User_Response {
    //输出流的初始化,为服务器对客户端的响应做准备
    OutputStream os=null;
    PrintWriter pw=null;
    Socket socket=null;
    int flag;
    public Select_User_Response(Socket socket,String name,
            String password_OR_path) throws IOException, SQLException{
        this.socket=socket;
        os=socket.getOutputStream();
        pw=new PrintWriter(os);
        /*
         * 创建和数据库的链接,完成对user表格的查询操作(完成登陆)
         * 若登录名不存在file表中(即未注册)返回2
         * 若登录名存在file表中,密码输入正确返回1
         * 若登录名存在file表中,密码输入错误返回3
         */
        Database_Select_User dsu=new Database_Select_User();
        flag=dsu.Database_Select_User_run(name,password_OR_path);
        
        /*
         * 对查找返回的数据进行分析,并对客户端进行响应
         */
        if(flag==1){
               pw.write("欢迎您,你已经完成登陆!请输入3");
               pw.flush();
        }
        else if(flag==2){
             pw.write("您还没进行注册,请先注册!");
               pw.flush();
        }
        else{
             pw.write("密码错误!请重新登陆!请输入2");
               pw.flush();             
        }
        //关闭输出输入流(关闭资源)
        pw.close();
        os.close();
    }

}

创建数据库连接对象

代码:

创建JDBC的工具类:

package com.database;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DBUtil {
    private static final String URL="jdbc:mysql://127.0.0.1:3306/socket?useUnicode=true&characterEncoding=utf-8";
    private static final String USER="root";
    private static final String PASSWORD="1234";
    
    private static Connection conn=null;
    
    static {
        try {
            //1.加载数据库厂商提供的驱动程序,com.myql.jdbc.Driver在mysql-connector-java的jar包中
            Class.forName("com.mysql.jdbc.Driver");
            //2.获得数据库的连接
            conn=DriverManager.getConnection(URL, USER, PASSWORD);
            } catch (ClassNotFoundException e) {
            e.printStackTrace();
            } catch (SQLException e) {
            e.printStackTrace();
            }
    }
    //将获得的数据库与java的链接返回(返回的类型为Connection)
    public static Connection getConnection(){
        return conn;
    }

}

数据库操作(预编译添加文件):

package com.database;
/*
 * 服务器端链接数据库,并向数据库表file添加数据
 * 添加成功返回false,否则返回true
 */
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;


public class Database_Add_File {
    boolean flag=false;
    public boolean Database_Add_File_run(String name,String password_OR_path) throws SQLException, FileNotFoundException{
        //获取mysql链接
        File files = new File(password_OR_path);
        Connection conn=DBUtil.getConnection();
        String sql=""+"insert into file"+"(user_name,path)"
                    +"values("+"?,?)";
        //加载sql语句到执行程序中(并不进行执行)
        PreparedStatement ptmt=conn.prepareStatement(sql);
        ptmt.setString(1, name);
        //ptmt.setString(2, password_OR_path);
        FileInputStream fis = new FileInputStream(files);
        ptmt.setBinaryStream(2, (InputStream) fis, (int) (files.length()));
        flag=ptmt.execute();
        return flag;
    }

}

数据库操作(预编译添加用户):

package com.database;
/*
 * 服务器端链接数据库,并向数据库表user添加数据
 * 添加成功返回true,否则返回false
 */
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class Database_Add_User {
    boolean flag=false;
    //
    public boolean Database_Add_User_run(String name,String password_OR_path) throws SQLException{
        //获取mysql链接
        System.out.println("开始运行Database_Add_User类");
        Connection conn=DBUtil.getConnection();
        String sql=""+"insert into user"+"(user_name,user_password)"
                    +"values("+"?,?)";
        //加载sql语句到执行程序中(并不进行执行)
        //PreparedStatement是预编译的,对于批量处理可以大大提高效率. 也叫JDBC存储过程
        PreparedStatement ptmt=conn.prepareStatement(sql);
        ptmt.setString(1, name);
        ptmt.setString(2, password_OR_path);        
        flag=ptmt.execute();
        return flag;
    }

}

数据库连接,校验用户名对应的密码是否正确:

package com.database;
/*
 * 用于用户登录时,将账号和密码传递到数据库,
 * 若登录名不存在file表中(即未注册)返回2
 * 若登录名存在file表中,密码输入正确返回1
 * 若登录名存在file表中,密码输入错误返回3
 */
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class Database_Select_User {
    
    
    public int Database_Select_User_run(String name,String password_OR_path) throws SQLException{
        //获取mysql链接
        Connection conn=DBUtil.getConnection();
        String sql1=""+"select * from user where user_name like ?";
        PreparedStatement ptmt=conn.prepareStatement(sql1.toString());
        ptmt.setString(1, "%"+name+"%");
        //executeQuery查询得到的结果集
        ResultSet rs=ptmt.executeQuery();
        while(rs.next()){
            //判断密码是否正确,正确返回1
            if(rs.getString("user_password").equals(password_OR_path)){
                return 1;
            }
            //判断密码错误返回2
            else
                return 3;
        }
        //用户名不存在返回2
        return 2;
    }

}

第二部分:客户端请求

第三部分:连接确认,服务器端监听到客户端的请求,然后响应客户端的套接字请求。

代码:

package com.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class Client {
    public static void main(String[] args) throws UnknownHostException, IOException {
        String []data=new String[3];
        int sum=0;//用来记录客户端第几次传值到服务器端
        /*
         * 客户端界面的提示语句
         */
        ClientTips ctps=new ClientTips();
        Scanner sc=new Scanner(System.in);
        int n;
        /*
         * 客户端实现与服务器端交互
         */
        try {
        while(true){//使得客户端始终处于程序运行当中
            //1.创建客户端socket,指定服务器和端口号
            Socket socket=new Socket("localhost", 8881);
            OutputStream os=null;
            InputStream is=null;
            InputStreamReader isr=null;
            BufferedReader br=null;
            ClientToServerInfo ctsi=null;
            ObjectOutputStream oos=null;
            while(true){//通过循环让客户端完成注册、登录    和上传信息的客户界面操作
                n=sc.nextInt();
                /*
                 * data用于接收tipchoose返回的字符串数组
                 * 如果传入的值为1时返回的为三个非空字符串,分别为“1”和用户名和密码
                 * 如果传入的值为2是返回的为两个非空字符串,分别是“2”和用户名和密码
                 * 如果传入的值为3是返回的两个字符串,分别是“3”和data[0]=null,data[1]=文件路径
                 */
            data=ctps.tipChoose(n);
            if(data[0]!=null&&data[1]!=null){
                //如果界面输入值合理,则跳出一层循环
                break;
              }
            }
                //2.获取输出流,向服务器端发送信息
                os=socket.getOutputStream();//字节输出流
                
                /*
                 *这里要判断是不是第一次写文件,若是则写入头部,否则不写入。 
                 */
                //if(sum==0){
                   oos=new ObjectOutputStream(os);
                //}
                //将需要传送的信息封装成对象
                ctsi=new ClientToServerInfo(data[0],data[1],data[2]);
                oos.writeObject(ctsi);
                oos.writeObject(null);//objectoutputstream写入结束的标志
                oos.flush();
                sum++;
                //socket.shutdownOutput();//关闭输出流
                //3.读取服务器端发送的信息
                is=socket.getInputStream();
                isr=new InputStreamReader(is);
                br=new BufferedReader(isr);
                 String info=null;
                if((info=br.readLine())!=null){
                    System.out.println("我是客户端,服务器说:"+info);
                }
                //用来区分不同次的客户端发送和服务器响应的命令
                System.out.println("**************************");
                //关闭输入输出流(关闭资源)
                br.close();
                isr.close();
                is.close();
                oos.close();
                os.close();
                socket.close();
        }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }        

}    

提示性语句:

package com.socket;
import java.util.Scanner;
public class ClientTips {
    static boolean flag=false;
    static String a;
    public ClientTips(){
    //打印客户端界面
            System.out.println("*********************************");
            System.out.println("注册信息:1");
            System.out.println("登陆账号:2");
            System.out.println("上传文件(只有在登陆之后才能传文件!):3");
            System.out.println("*********************************");
    }
    public String[] tipChoose(int n){
    String[] userinfo=new String[3];
    String b,c,e;
    Scanner sc=new Scanner(System.in);
    /*
     * 注册时的提示操作
     */
    if(n==1){
        System.out.print("请输入要注册的账号名:");
        a=sc.nextLine();
        System.out.print("请输入密码:");
        b=sc.nextLine();
        System.out.print("请确认密码:");
        c=sc.nextLine();
        if(!c.equals(b)){//这里不能用c!=b判断,c!=b不仅对比了字符串,还对比了存储地址
            System.out.println("密码不一致!注册失败,需要重新注册请输入:1");
            //如果注册失败,userinfo返回的为空数组
            userinfo[0]=null;
            userinfo[1]=null;
            userinfo[2]=null;
        }
        else{
            userinfo[0]="1";
            userinfo[1]=a;
            userinfo[2]=b;
        }
    }
    /*
     * 登陆时提示信息操作
     */
    if(n==2){
        System.out.print("请输入要登陆的账号名:");
        a=sc.nextLine();
        System.out.print("请输入密码:");
        b=sc.nextLine();
        userinfo[0]="2";
        userinfo[1]=a;
        userinfo[2]=b;
        flag=true;
    }
    /*
     * 传送文件相关文件
     */
    if(n==3){
        if(flag!=true){
            System.out.println("请先登录账号!请输入2");
            //如果没有登陆操作,输入3时先返回空数组
            userinfo[0]=null;
            userinfo[1]=null;
            userinfo[2]=null;
        }
        else{
            System.out.println("请输入文件的路径:");
            e=sc.nextLine();
            userinfo[0]="3";
            userinfo[1]=a;
            userinfo[2]=e;
        }
    }
    return userinfo;    
    }
}

补充:上面的代码是客户端和服务器端进行socket网络通信,可以开启两个进程去执行。当然,我们也可以写个简单的demo在自己本机上测试,如下,最后在item或者cmd里输入nc 本机ip 端口号访问服务器(nc命令实现任意TCP/UDP端口的监听)。

public class SocketIO {
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(9090);

        System.out.println("step1 : new ServerSocket(9090)");

        Socket client = server.accept(); //阻塞1

        System.out.println("step2:client\t"+client.getPort());

        InputStream in = client.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        System.out.println(reader.readLine());  //阻塞2

        //在item上输入:nc 本机ip 端口号,可以访问该服务器
        while(true){
        }
    }
}