JAVA网络编程之Socket

    |     2015年5月27日   |   多线程编程   |     0 条评论   |    1084

网络编程有两个主要问题:如何准确定位网络上一台或多台主机,以及找到主机后如何可靠、高效地传输数据。在 TCP/IP 里,IP 层负责主机定位和路由,IP 地址唯一确定 Internet 上的一台主机。

TCP 层则提供面向应用的可靠(TCP)或非可靠(UDP)传输,这才是网络编程的主要对象,一般不必关心 IP 如何处理数据。流行模型是客户机/服务器(C/S):服务器作为守护进程始终运行、监听端口;有客户请求就启动一个服务进程响应,同时自己继续听,让后来的客户也能及时得到服务。

一、两类传输协议:TCP 与 UDP

TCP(Transfer Control Protocol)是面向连接、保证可靠传输的协议。得到的是顺序、无差错的数据流。成对的两个 socket 必须先建立连接,然后双向收发。

UDP(User Datagram Protocol)是无连接协议。每个数据报都是独立信息,含完整源/目的地址,在网上走任意可能路径,因此能否到达、何时到达、内容是否正确都不能保证。

对比 UDP TCP
连接 数据报自带完整地址,不必先连接 传输前必须建立连接,有握手时间
大小 每个数据报须在 64KB 之内 连接建立后可按统一格式传大数据
可靠性 不可靠,到达次序也不保证 确保接收方正确拿到全部数据
典型应用 视频会议、游戏:要连贯不必绝对正确 Telnet、FTP:不定长数据必须可靠

可靠传输要付出代价:校验占用 CPU 和带宽。UDP 操作简单、监护少,适合局域网高可靠分散系统里的 C/S。

二、什么是 Socket

网络上两个程序通过双向通讯连接交换数据,链路的一端称为一个 Socket。Socket 通常用来实现客户方和服务方的连接,由一个 IP 和一个端口号唯一确定。Socket 支持的协议不只 TCP/IP 一种,两者没有必然联系。Java 环境下的 Socket 编程主要指基于 TCP/IP 的网络编程。

过程:Server 监听某端口是否有连接请求,Client 发出 Connect,Server 回 Accept,连接建立。双方可用 Send、Write 等与对方通信。完整 Socket 工作四步:

  • 创建 Socket
  • 打开连接到 Socket 的输入/输出流
  • 按协议对 Socket 读/写
  • 关闭 Socket(简单演示程序有时省略显式 close)

三、创建 Socket

java.net 提供 Socket(客户端)和 ServerSocket(服务端):

Socket(InetAddress address, int port);
Socket(InetAddress address, int port, boolean stream);
Socket(String host, int port);
Socket(String host, int port, boolean stream);
Socket(SocketImpl impl);
Socket(String host, int port, InetAddress localAddr, int localPort);
Socket(InetAddress address, int port, InetAddress localAddr, int localPort);
ServerSocket(int port);
ServerSocket(int port, int backlog);
ServerSocket(int port, int backlog, InetAddress bindAddr);

address、host、port 是对方 IP、主机名、端口;stream 指明流 socket 还是数据报 socket;localPort 是本地端口;localAddr / bindAddr 是本机地址;impl 是 socket 父类。backlog 表示服务端所能支持的最大等待连接数。例如:

Socket client = new Socket("127.0.0.1", 80);
ServerSocket server = new ServerSocket(80);

选端口要小心。0~1023 为系统保留:HTTP 80,FTP 21,Telnet 23(原文把 ftp/telnet 端口写反了)。自定义服务应选大于 1023 的端口。创建失败会抛 IOException,必须捕获或声明抛出。

四、简单 Client / Server

客户端向 127.0.0.1:4700 发请求,从标准输入读一行,不是 bye 就发给 Server,再打印 Server 回的一行。原文客户端把输入流变量写成 is,后面误用;下面统一为 is。

import java.io.*;
import java.net.*;
public class TalkClient {
    public static void main(String args[]) {
        try {
            Socket socket = new Socket("127.0.0.1", 4700);
            // 向本机的 4700 端口发出客户请求
            BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
            PrintWriter os = new PrintWriter(socket.getOutputStream());
            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String readline = sin.readLine();
            while (!readline.equals("bye")) {
                os.println(readline);
                os.flush();
                System.out.println("Client:" + readline);
                System.out.println("Server:" + is.readLine());
                readline = sin.readLine();
            }
            os.close();
            is.close();
            socket.close();
        } catch (Exception e) {
            System.out.println("Error" + e);
        }
    }
}

服务器在 4700 监听,accept 后进入同样的对话循环:

import java.io.*;
import java.net.*;
public class TalkServer {
    public static void main(String args[]) {
        try {
            ServerSocket server = null;
            try {
                server = new ServerSocket(4700);
            } catch (Exception e) {
                System.out.println("can not listen to:" + e);
            }
            Socket socket = null;
            try {
                socket = server.accept(); // accept() 阻塞等待客户请求
            } catch (Exception e) {
                System.out.println("Error." + e);
            }
            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter os = new PrintWriter(socket.getOutputStream());
            BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("Client:" + is.readLine());
            String line = sin.readLine();
            while (!line.equals("bye")) {
                os.println(line);
                os.flush();
                System.out.println("Server:" + line);
                System.out.println("Client:" + is.readLine());
                line = sin.readLine();
            }
            os.close();
            is.close();
            socket.close();
            server.close();
        } catch (Exception e) {
            System.out.println("Error:" + e);
        }
    }
}

五、支持多客户

实际服务器要同时服务多个客户:永久监听,一有请求就启动专门的服务线程,自己马上回到监听。

ServerSocket serverSocket = null;
boolean listening = true;
try {
    serverSocket = new ServerSocket(4700);
} catch (IOException e) { }
while (listening) { // 永远循环监听
    new ServerThread(serverSocket.accept(), clientnum).start();
    clientnum++;
}
serverSocket.close();
public class ServerThread extends Thread {
    Socket socket = null;
    int clientnum;
    public ServerThread(Socket socket, int num) {
        this.socket = socket;
        clientnum = num + 1;
    }
    public void run() {
        try {
            // 在这里实现数据的接收和发送
        } catch (Exception e) { }
    }
}

六、Datagram 通讯

传输层除 TCP 还有 UDP。HTTP、FTP、SMTP 等用 TCP;需要强实时交互时(网游、视频会议)常用 UDP。数据报像邮件,不保证必达;面向连接的 TCP 像电话。

DatagramSocket 建立传送数据报的连接,DatagramPacket 表示一个数据报。构造时若发生端口冲突会抛 SocketException,必须处理。

DatagramSocket();
DatagramSocket(int port);
DatagramSocket(int port, InetAddress laddr);
DatagramPacket(byte buf[], int length);
DatagramPacket(byte buf[], int length, InetAddress addr, int port);
DatagramPacket(byte[] buf, int offset, int length);
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port);

buf 存放数据,length 是长度,addr 和 port 指明目的地址,offset 是位移。接收前用第一种构造生成 Packet,再 receive() 等待(一直等到有数据报):

DatagramPacket packet = new DatagramPacket(buf, 256);
socket.receive(packet);

发送用带目的地址的构造,再 send():

DatagramPacket packet = new DatagramPacket(buf, length, address, port);
socket.send(packet);

构造数据报时要给 InetAddress,可用 getByName() 从主机名得到 IP。

七、组播 MulticastSocket

DatagramSocket 只能发到一个目的地址。MulticastSocket 允许以广播方式发到该端口的所有客户,用在客户端监听服务器广播。

客户方 MulticastClient.java:加入组 230.0.0.1,循环 5 次接收。

import java.io.*;
import java.net.*;
public class MulticastClient {
    public static void main(String args[]) throws IOException {
        MulticastSocket socket = new MulticastSocket(4446);
        InetAddress address = InetAddress.getByName("230.0.0.1");
        socket.joinGroup(address);
        DatagramPacket packet;
        for (int i = 0; i < 5; i++) {
            byte[] buf = new byte[256];
            packet = new DatagramPacket(buf, buf.length);
            socket.receive(packet);
            String received = new String(packet.getData());
            System.out.println("Quote of the Moment:" + received);
        }
        socket.leaveGroup(address);
        socket.close();
    }
}

服务器方启动线程:

public class MulticastServer {
    public static void main(String args[]) throws java.io.IOException {
        new MulticastServerThread().start();
    }
}

MulticastServerThread(原文 Private / FIVE_SECOND / getByte 为笔误,下面按可运行写法):

import java.io.*;
import java.net.*;
import java.util.*;
public class MulticastServerThread extends QuoteServerThread {
    private long FIVE_SECONDS = 5000;
    public MulticastServerThread(String name) throws IOException {
        super("MulticastServerThread");
    }
    public void run() {
        while (moreQuotes) {
            try {
                byte[] buf = new byte[256];
                String dString = null;
                if (in == null) dString = new Date().toString();
                else dString = getNextQuote();
                buf = dString.getBytes();
                InetAddress group = InetAddress.getByName("230.0.0.1");
                DatagramPacket packet = new DatagramPacket(buf, buf.length, group, 4446);
                socket.send(packet);
                try {
                    sleep((long)(Math.random() * FIVE_SECONDS));
                } catch (InterruptedException e) { }
            } catch (IOException e) {
                e.printStackTrace();
                moreQuotes = false;
            }
        }
        socket.close();
    }
}

一句话总结:定位靠 IP,传输选 TCP(可靠连接)或 UDP(无连接数据报);Java 里 TCP 用 Socket/ServerSocket 四步走,UDP 用 DatagramPacket,组播再用 MulticastSocket。

转载请注明来源:JAVA网络编程之Socket
本文链接地址:https://ai.zhousir.top/?p=694

上一篇:

下一篇:

回复 取消