用Java实现简单的网络聊天程序

理论原理:

Socket套接字定义:

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

    • Socket的英文原义是“孔”或“插座”。在网络编程中,网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个Socket。

    • Socket套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。
      它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

    • Socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。 

Socket 调用流程:

 

socket()创建套接字

bind()指定本地地址。一个套接字用socket()创建后,它其实还没有与任何特定的本地或目的地址相关联。在很多情况下,应用程序并不关心它们使用的本地地址,这时就可以不用调用bind指定本地的地址,而由协议软件为它们选择一个。但是,在某个知名端口(Well-known Port)上操作的服务器进程必须要对系统指定本地端口。所以一旦创建了一个套接字,服务器就必须使用bind()系统调用为套接字建立一个本地地址。

connect()将套接字连接到目的地址。初始创建的套接字并未与任何外地目的地址关联。客户机可以调用connect()为套接字绑定一个永久的目的地址,将它置于已连接状态。对数据流方式的套接字,必须在传输数据前,调用connect()构造一个与目的地的TCP连接,并在不能构造连接时返回一个差错代码。如果是数据报方式,则不是必须在传输数据前调用connect。如果调用了connect(),也并不像数据流方式那样发送请求建连的报文,而是只在本地存储目的地址,以后该socket上发送的所有数据都送往这个地址,程序员就可以免去为每一次发送数据都指定目的地址的麻烦。

listen()设置等待连接状态。对于一个服务器的程序,当申请到套接字,并调用bind()与本地地址绑定后,就应该等待某个客户机的程序来要求连接。listen()就是把一个套接字设置为这种状态的函数。

accept()接受连接请求。服务器进程使用系统调用socket,bind和listen创建一个套接字,将它绑定到知名的端口,并指定连接请求的队列长度。然后,服务器调用accept进入等待状态,直到到达一个连接请求。

send()/recv()和sendto()/recvfrom()发送和接收数据 。在数据流方式中,一个连接建立以后,或者在数据报方式下,调用了connect()进行了套接字与目的地址的绑定后,就可以调用send()和reev()函数进行数据传输。

closesocket()关闭套接字

 

TCP 三次握手四次断开的工作原理:

代码实现

服务端 server.java 代码:

1.创建ServerSocket对象,绑定监听端口。
2.通过accept()方法监听客户端请求。
3.连接建立后,在接收进程中通过输入流读取客户端发送的请求信息。
4.在服务器发送进程中通过输出流向客户端发送响应信息。
5.关闭相应的资源和Socket。

package hello_hi;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class server {
    public static void main(String[] args) throws Exception {
        // 监听指定的端口,阻塞直至客户端连接此端口
        int port = 2199;
        ServerSocket server = new ServerSocket(port);
                           // new ServerSocket(port)创建端口,包括socket() bind() listen() 3个API的调用
        System.out.println("服务端在等待连接客户端...");
        Socket socket = server.accept(); //调用API:accept()等待client的消息
        System.out.println("连接到客户端!");

        // 读取从客户端返回的输入流,至尾端时返回-1
        InputStream input = socket.getInputStream();
        byte[] bytes = new byte[1024];
        int len;
        StringBuilder get = new StringBuilder();
        while ((len = input.read(bytes)) != -1) { //调用API:input.read()
            get.append(new String(bytes, 0, len, "UTF-8"));
        }
        System.out.println("弟弟1号: " + get);

        OutputStream output = socket.getOutputStream();
        String sent = "Hello 弟弟~";
        output.write(sent.getBytes("UTF-8"));
        System.out.println("发送消息: " + sent);

        // 关闭socket与服务器端
        input.close();
        output.close();
        socket.close();
        server.close();
    }
}

客户端 client001.java 代码:

1.创建Socket对象,指明需要连接的服务器的地址和端口号。
2.连接建立后,通过输出流向服务器发送请求信息。
3.通过输入流获取服务器响应的信息。
4.关闭相应资源。

package hello_hi;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class client001 {
    public static void main(String args[]) throws Exception {
        // 连接本地主机,端口自设,与服务端一致即可
        String host = "127.0.0.1"; //本机IP
        int port = 2199; //端口号port
        // 通过socket与服务端建立连接
        Socket socket = new Socket(host, port);// host+port = IP+端口号
        if(socket.isConnected()){
            System.out.println("连接到服务端!");
        }
        // 将要发送的信息写入输出流
        OutputStream output = socket.getOutputStream();
        String sent = "Hi 大哥好!";
        socket.getOutputStream().write(sent.getBytes("UTF-8"));
        // 关闭客户端的输出流(单向,并未关闭socket)
        socket.shutdownOutput();
        System.out.println("发送消息: " + sent);

        // 读取从server返回的输入流
        InputStream input = socket.getInputStream();
        byte[] bytes = new byte[1024];
        int len;
        StringBuilder get = new StringBuilder();
        while ((len = input.read(bytes)) != -1) {
            get.append(new String(bytes, 0, len,"UTF-8"));
        }
        System.out.println("大哥: " + get);

        // 关闭socket
        input.close();
        output.close();
        socket.close();
    }
}

程序运行结果(使用IDEA编辑器):

(1)服务端等待客户端连接(运行 server.java,但未运行 client001.java):

 

 (2)服务端连接上客户端,接收、发送消息:

 

 (3)客户端连接上服务器,发送消息、接收消息

总结分析

优化思路:

由于第一次接触socket编程(also Java...)比较仓促,至少有以下几点可以提高的地方:

1.通信循环输入输出,使用while()结构实现。

2.有了1的话,可以持续对话,但是只能你一句我一句,不能一个人连续多句话。所以还需要连续发送功能,这个需要整个重构,一方发送一方及时接收。

3.不能多人(群)对话,具体实现有待学习。。

JAVA Socket API分析

以 Socket 为例子:

 再查看 SocketImpl:

 

  再查看 ServerSocket:

 

 其中的大量调用关系比较复杂,还不是完全清楚。。

Socket总结:

套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。 

服务器监听:建立服务器端套接字,并处于等待连接的状态,不定位具体的客户端套接字,而是实时监控网络状态。 

客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。 为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。 

连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端, 一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。  上图为基于TCP协议Socket的通信模型。

原文地址:https://www.cnblogs.com/qyf2199/p/12023681.html