java中的socket编程

来源:互联网 发布:神盾阅读器软件 编辑:程序博客网 时间:2024/06/05 03:53

随便玩玩

本周末研究了一会儿java的socket编程,由于java web开发往往使用servlet等应用层的类库和框架,因此个人兴趣,想研究一下传输层的内容。

socket client

首先简单的,使用java的socket编程建立一个客户端,能够访问服务器相应内容。
简单的几行代码如下:

//initial socket client, set host, port and time out intervalSocket client = new Socket("127.0.0.1", 9999);client.setSoTimeout(10000);

两行代码基本上建立了一个socket通道。其中host入参可以是IP或hostname。

可以通过下列两行代码获取socket的输入、输出流

//get socket client output streamPrintStream socketOut = new PrintStream(client.getOutputStream());//get socket client input streamBufferedReader socketIn = new BufferedReader(new InputStreamReader(client.getInputStream()));

获取输入、输出流之后,即可通过一般的流操作进行socket通信。

通信结束后记得将输入、输出流以及socket关闭

// close input stream, output stream and socketsocketIn.close();socketOut.close();client.close();

最好将上述代码try catch住,在finally块中进行一次判断,完全保证成功关闭。

socket server

有了客户端可能找不到合适的服务端进行实验,那就自己建立个socket server。
仍旧是短短几行代码:

// initial socket serverServerSocket server = new ServerSocket(9999);// receive an socket connectionSocket client = server.accept();

如上述代码,指定端口号即可创建一个ServerSocket,该server会处于监听状态,每当有client连接时,accept()方法会返回请求的client socket。
获取输入、输出流的代码与上一节一样。这个socket的在server端的输入流对应client端的输出流,server端的输出流对应client端的输入流,如下:
fcb4fb28-1a00-46b4-a90e-9081d6fc19c0.png

同样在结束时需要关闭输入、输出流,及socket。

多线程server

上述server没有什么实用性,收到一个client连接,通信完成后即结束,这样的server基本没什么用。
因此一般的server应该每收到一个请求,就创建一个线程去处理,同样几行代码即可完成:

public class SocketServerThread implements Runnable {    private Socket              client;    public SocketServerThread(Socket client) {        this.client = client;    }    @Override    public void run() {        // code here    }}
// start a new thread to process this socketSocketServerThread thread = new SocketServerThread(client);new Thread(thread).start();

这样做才是一个正常服务器应有的功能,对每个请求并行处理,如下:
7a16d25b-8a38-4de7-90d5-c66b93d7239a.png

交互式socket

这是一个有趣的demo,从console输入内容,发送给server,回显server返回的内容。输入”exit”,则关闭socket。
还是简单的几行代码:
client端:

//get system inputBufferedReader input = new BufferedReader(new InputStreamReader(System.in));// continue if flag is true, exit otherwiseboolean flag = true;while (flag) {    System.out.println("Please input : ");    // get system input by user    String str = input.readLine();    //send content to socket    socketOut.println(str);    // set flag to false if input "exit"    if (str.trim().equals(EXIT)) {        flag = false;    } else {        // socket need response in some interval, or it will throw exception        try {            String response = socketIn.readLine();            System.out.println(response);        } catch (SocketTimeoutException e) {            System.out.println("socket time out");        }    }}

server端

// continue if flag is true, exit otherwiseboolean flag = true;while (flag) {    String str = socketIn.readLine().replace("\n", "");    if (str == null || str.trim().equals("") || str.trim().equals(EXIT)) {        flag = false;        System.out.println("this client has exited, and this server thread exits");        socketOut.println("bye bye ~");    } else {        System.out.println("server has receive a message : " + str);        // response the string to client        socketOut.println(str);    }}

这个demo没有什么实用性的功能,但通过这个demo能够感受到socket的连接,客户端与服务器的交互。

代理

代理这个词很常见,例如翻墙,访问校园网,公司内网等场景都会用到,大家常说的VPN也就是代理的意思。

代理的原理也很简单,顾名思义,你访问不了某些网站,例如google,但是其他的主机能够访问,比如位于美国的主机。那么你可以将这些主机作为代理,你每次的访问请求发送给代理,代理帮你发送给目标网站,再把目标网站的回复返回给你。就相当于代理你去做某些事,因此就有了代理这个名字。如下图:
8dc3afa1-6542-4c7d-b333-9e71d9718e9e.png

client使用代理

java socket虽然没有应用层用的那么频繁,但是类库封装的还是很给力的。
仍旧是简单的几行代码:
创建一个proxy对象

// initial a socket addressSocketAddress address = new InetSocketAddress(PROXY_ADDRESS, PROXY_PORT);// initial a proxyProxy proxy = new Proxy(Proxy.Type.SOCKS, address);

将代理传入客户端socket,发起请求

// initial a socket client with proxySocket client = new Socket(proxy);// initial real destination addressSocketAddress destAddress = new InetSocketAddress(DEST_ADDRESS, DEST_PORT);// connect to destinationclient.connect(destAddress, TIME_OUT);

之后的过程跟一般的client socket没什么区别。只是在connect之前会先向代理服务器发起请求,得到成功的回复后,就跟上文没什么区别了。

推荐一个代理网站 http://www.xicidaili.com/ , 免费的哦

socks5

什么是socks5? socks5也可以叫socket5,是对于socket的代理协议,你要使用代理,就要懂得代理的协议,遵守协议的规定。

socks5协议的具体内容你可以看官方文档https://www.ietf.org/rfc/rfc1928.txt, 有点难懂哈

也可以看几篇博客,[http://www.mojidong.com/network/2015/03/07/socket5-1/](http://www.mojidong.com/network/2015/03/07/socket5-1/)
http://zhihan.me/network/2017/09/24/socks5-protocol/

下面为简单说一下socks5的内容:
- client第一次向代理服务器发送请求时,内容应该是

版本号 可用连接方法数量 连接方法列表 0x05 {1 byte} 可变长度(0x00 为不验证,0x02为用户名/密码验证)

可用连接方法数量决定后面连接方法列表的长度
- 如 0x05 0x01 0x00 代表只有一种方法————不验证
连接方法还有其他,可以查看推荐的文档

  • 代理服务器收到请求,解析,从client给出的方法中选择自己支持的方法,返回格式如下
版本号 支持的验证方式 0x05 0x00

若客户端提供验证方式没有服务器支持的,则返回0xff, 代表不支持,客户端需关闭连接

  • client收到server支持的验证方式,若0x00无需验证,则建立连接成功。若为0x02用户名/密码验证,则需要发送密码,格式如下:
版本号 用户名长度 用户名 密码长度 密码 0x05 {1byte} {可变长度} {1byte} {可变长度}

- 服务器接收client的用户名/密码,验证后返回结果

版本号 验证结果 0x05 0x00(0x00代表成功, 0x01代表失败)

- 建立连接成功后,发送目标服务器信息

协议版本号 请求的类型 保留字段 地址类型 地址数据 地址端口 0x05 0x01代表建立代理连接 0x02代表建立连接监听目标地址的请求 0x03代表upd代理连接 {1byte} {1byte}代表地址类型 0x01代表IPv4, 0x03代表域名, 0x04代表IPv6 变长,可以为hostName(第一个byte代表长度)或IPv4(4byte), IPv6(16byte) {2byte}

- 代理服务器收到请求,创建连接,并返回结果

协议版本号 状态码 保留字段 地址类型 绑定的地址 绑定的端口 0x05 {1byte} {1byte} 同上 同上 同上

状态码如下:
X00 succeeded
X01 general SOCKS server failure
X02 connection not allowed by ruleset
X03 Network unreachable
X04 Host unreachable
X05 Connection refused
X06 TTL expired
X07 Command not supported
X08 Address type not supported
X09 to X’FF’ unassigned

tcp连接的话接下来直接转发即可。

代理服务器

代理既然这么好用,干嘛不自己搭一个代理服务器,其实代理服务器也很跟一般服务器没什么区别,就多了代理两个字而已。。。

不过代理服务器确实要比一般服务器多点东西,因为对于客户端来说,它是服务器,对于真正的服务器来说它又是客户端,总是要做点转发请求的额外工作。同时,你如果不想让别人随随便便就用你的代理服务器,还要做点验证什么的吧。

先来基础的,能起到代理的作用再说。同样对每一个请求都要新启一个线程处理哦,那这样直接写自定义Thread类的内容:
socks5验证及建立连接的代码:

private Socket sock5_check(InputStream in, OutputStream out) throws IOException {    byte[] tmp = new byte[2];    in.read(tmp);    // false if has not login, true otherwise    boolean isLogin = false;    // authorize method, 0x00-no authorized, 0x02-user/password     byte method = tmp[1];    // 0x02 means the client choose user/password authorized    if (0x02 == tmp[0]) {        method = 0x00;        in.read();    }    // if need login check, which set by proxy server, set method to 0x02    if (socksNeekLogin) {        method = 0x02;    }    // {0x05, 0x00} means no authorized, {0x05, 0x02} means user/password authorized    tmp = new byte[] { 0x05, method };    out.write(tmp);    out.flush();    // Socket result = null;    Object resultTmp = null;    // method is 0x02 means need login check    if (0x02 == method) {// 处理登录.        int b = in.read();        String user = null;        String pwd = null;        if (0x01 == b) {            // read username's length            b = in.read();            tmp = new byte[b];            // read user name            in.read(tmp);            user = new String(tmp);            // read password's length            b = in.read();            tmp = new byte[b];            // read password            in.read(tmp);            pwd = new String(tmp);            if (null != user && user.trim().equals(this.user) && null != pwd                && pwd.trim().equals(this.pwd)) {// 权限过滤                isLogin = true;                // 0x05 means socks5 and 0x00 means login successfully ????                tmp = new byte[] { 0x05, 0x00 };// 登录成功                out.write(tmp);                out.flush();                log("%s login success !", user);            } else {                log("%s login faild !", user);            }        }    }    byte cmd = 0;    if (!socksNeekLogin || isLogin) {// 验证是否需要登录        // read proxy header         // format :  |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |        tmp = new byte[4];        in.read(tmp);        log("proxy header >>  %s", Arrays.toString(tmp));        cmd = tmp[1];        // get host name by protocol type, 0x01 means IPv4, 0x03 means hostName and 0x04 means IPv6        String host = getHost(tmp[3], in);        // read port number        tmp = new byte[2];        in.read(tmp);        int port = ByteBuffer.wrap(tmp).asShortBuffer().get() & 0xFFFF;        log("connect %s:%s", host, port);        // create response content        // format |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |        ByteBuffer rsv = ByteBuffer.allocate(10);        // put socks version 0x05        rsv.put((byte) 0x05);        try {            if (0x01 == cmd) {// cmd is 0x01 means request is to connect                resultTmp = new Socket(host, port);                rsv.put((byte) 0x00);// 0x00 means succeeded            } else if (0x02 == cmd) {// cmd 0x02 means bind                resultTmp = new ServerSocket(port);                rsv.put((byte) 0x00);            } else {                rsv.put((byte) 0x05);// 0x05 means connection refused                resultTmp = null;            }        } catch (Exception e) {            rsv.put((byte) 0x05);            resultTmp = null;        }        rsv.put((byte) 0x00);        rsv.put((byte) 0x01); // 0x01 means IP v4        rsv.put(socket.getLocalAddress().getAddress());// put local address        Short localPort = (short) ((socket.getLocalPort()) & 0xFFFF);        rsv.putShort(localPort); // put local port        tmp = rsv.array();    } else {        // 0x05 means socks5 and 0x01 means login failed ???        tmp = new byte[] { 0x05, 0x01 };// 登录失败        log("socks server need login,but no login info .");    }    out.write(tmp);    out.flush();    if (null != resultTmp && 0x02 == cmd) {        ServerSocket ss = (ServerSocket) resultTmp;        try {            resultTmp = ss.accept();        } catch (Exception e) {        } finally {            // close server socket            closeIo(ss);        }    }    return (Socket) resultTmp;}

上述代码包含了验证代码和与server建立连接,并返回与server建立的socket对象。
其中获取目标地址的方法如下:

  private String getHost(byte type, InputStream in) throws IOException {      String host = null;      byte[] tmp = null;      switch (type) {          case 0x01:// IPV4协议              tmp = new byte[4];              in.read(tmp);              host = InetAddress.getByAddress(tmp).getHostAddress();              break;          case 0x03:// 使用域名              int l = in.read();              tmp = new byte[l];              in.read(tmp);              host = new String(tmp);              break;          case 0x04:// 使用IPV6              tmp = new byte[16];              in.read(tmp);              host = InetAddress.getByAddress(tmp).getHostAddress();              break;          default:              break;      }      return host;  }

有点难懂,不过结合上述解释应该能够看懂。
连接建立后,只需要新启两个线程,做client 与 server 的报文交换即可。
交互代码如下:

protected static final void transfer(final CountDownLatch latch, final InputStream in,                                     final OutputStream out, final OutputStream cache,                                     String direction) {    new Thread() {        @Override        public void run() {            System.out.println("proxy socket transfer channel has established");            byte[] bytes = new byte[2048];            int n = 0;            try {                while ((n = in.read(bytes)) > 0) {                    out.write(bytes, 0, n);                    out.flush();                    System.out.println(direction + " : " + byte2String(bytes, 0, n - 1));                    if (null != cache) {                        synchronized (cache) {                            cache.write(bytes, 0, n);                        }                    }                }            } catch (Exception e) {            }            if (null != latch) {                latch.countDown();            }        };    }.start();}

设置用户名和密码

有没有发现,如果选择用户名/密码验证,在client中并没有设置用户名/密码的代码。
其实我们可以自己在代码中设置,如果没设置,则默认会用你当前登录系统的用户名/密码。
自定义设置的方法如下:

public static void resetAuth() {    Authenticator.setDefault(new Authenticator() {        @Override        protected PasswordAuthentication getPasswordAuthentication() {            return new PasswordAuthentication(USER, PASSWD.toCharArray());        }    });}
原创粉丝点击