基于java实现hello/hi简单网络聊天程序

RichardTao      2022-05-20     319

关键词:

Socket简要阐述

Socket的概念

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

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

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

HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

Socket原理

Socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。正如打电话之前,双方必须各自拥有一台电话机一样。

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

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

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

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

下图为基于TCP协议Socket的通信模型。
通信模型

hello/hi的简单网络聊天程序实现

服务器端

实现步骤

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

package com.socket.MultiThread;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class Server {

    public static ServerSocket serverSocket;
    public static Socket socket;
    public static Scanner scanner;

    /**
     * 构造方法
     * 新建serverSocket和Socket
     */
    public Server() {
        try {
            serverSocket = new ServerSocket(6666);
            System.out.println("Server is working, waiting for client's link");
            socket = serverSocket.accept();
            System.out.println("Client has linked with Server");
        } catch (IOException i) {
            i.printStackTrace();
        }
    }

    /**
     * 服务器端发送消息线程
     * 作用:从键盘读入消息,发送给服务器端
     */
    public class SendThread implements Runnable {
        @Override
        public void run() {
            try {
                OutputStream outputStream = socket.getOutputStream();
                OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
                PrintWriter printWriter = new PrintWriter(outputStreamWriter, true);

                scanner = new Scanner(System.in);

                while (true) {
                    printWriter.println(scanner.nextLine());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 服务器端接收线程
     * 作用:使用字符流读取缓冲区中客户端所发送的消息
     */
    public class ReceiveThread implements Runnable {
        @Override
        public void run() {
            try {
                InputStream inputStream = socket.getInputStream();
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

                // 输出从客户端端接收到的消息
                while (true) {
                    System.out.println("Client> " + bufferedReader.readLine());
                }
            } catch (IOException i) {
                i.printStackTrace();
            }
        }
    }

    public void start() {
        Thread send = new Thread(new SendThread()); // 发送进程负责服务器端的消息发送
        Thread receive = new Thread(new ReceiveThread()); // 接收进程负责接收客户端的消息
        send.start();
        receive.start();
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

}

客户端

实现步骤

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

package com.socket.MultiThread;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class Client {

    public static Socket socket;
    public static Scanner scanner;

    /**
     * 构造方法
     * 新建一个socket,并指定了host和port属性,其port与服务器端保持一致
     */
    public Client() {
        try {
            socket = new Socket("127.0.0.1", 6666);
        } catch (IOException i) {
            i.printStackTrace();
        }
    }

    /**
     * 客户端发送消息线程
     * 作用:从键盘读入消息,发送给服务器端
     */
    public class SendThread implements Runnable {
        @Override
        public void run() {
            try {
                OutputStream outputStream = socket.getOutputStream();
                OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
                PrintWriter printWriter = new PrintWriter(outputStreamWriter, true);

                scanner = new Scanner(System.in);

                while (true) {
                    printWriter.println(scanner.nextLine());
                }
            } catch (IOException i) {
                i.printStackTrace();
            }
        }
    }

    /**
     * 客户端接收线程
     * 作用:使用字符流读取缓冲区中服务器端所发送的消息
     */
    public class ReceiveThread implements Runnable {
        @Override
        public void run() {
            try {
                InputStream inputStream = socket.getInputStream();
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"UTF-8");
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

                // 输出从服务器端接收到的消息
                while (true) {
                    System.out.println("Server> " + bufferedReader.readLine());
                }
            } catch (IOException i) {
                i.printStackTrace();
            }
        }
    }


    public void start() {
        Thread send = new Thread(new SendThread()); // 发送进程负责客户端的消息发送
        Thread receive = new Thread(new ReceiveThread()); // 接收进程负责接收服务器端的消息
        send.start();
        receive.start();
    }

    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }

}

程序执行结果

先运行服务器端,后运行客户端,服务器端在监听客户端的连接请求后建立连接。

Connect

服务器端与客户端的交互

客户端显示消息
服务器端显示消息

跟踪分析调用栈 & Linux API对比

创建ServerSocket

在前面的服务器端代码中,我们创建一个ServerSocket是这样做的:
serverSocket = new ServerSocket(6666);

这行代码在平常看来就是创建了一个端口号为6666的ServerSocket。
但是实际上,我们只是调用了大牛们早已经写好并封装在JDK中的方法,这才能够如此简单地完成套接字的创建。

因此下面通过查看JDK源码,追踪其调用栈来看看ServerSocket的创建究竟是如何实现的。

调用栈图示

ServerSocket创建
通过上图中对于jdk代码中socket创建过程的展示,我们了解到:
在java中ServerSocket的创建主要是调用PlainSocketImpl.socketCreate这个native方法来实现的。

源码分析

那么,我们来康一下这个方法:

/**
* The file descriptor object for this socket.
*/
    protected FileDescriptor fd; // 文件描述符

@Override
    void socketCreate(boolean stream) throws IOException {
        if (fd == null) // 空则抛出异常
            throw new SocketException("Socket closed");

        int newfd = socket0(stream); // 调用jvm的socket0方法来创建新的fd

        fdAccess.set(fd, newfd);
    }

可以看到PlainSocketImpl.socketCreate方法中有一个重要的变量是fd,在代码块中我也将这个变量的声明一并列出了。
记得在本科的Linux课上老师曾经也着重强调了文件描述符这个概念,那么此fd是彼fd吗?
在了解了Linux内核中Socket的建立之后,就能够得出答案:是的。

在jvm中,调用linux底层api: socket()函数时,执行的步骤为:

创建socket结构体
创建tcp_sock结构体,刚创建完的tcp_sock的状态为:TCP_CLOSE
创建文件描述符与socket绑定

因此,在PlainSocketImpl.socketCreate方法中所实现的也正是这样的逻辑。

Socket绑定

上述分析中,我们会发现:
PlainSocketImpl.socketCreate中创建socket时,它并没有绑定任何的ip地址与端口,只是实现了与文件描述符的绑定。
这就有点奇怪了,我们在上面的Java代码中创建ServerSocket的时候明明指定了端口号的呀,怎么调用到底层方法它就把端口号丢了呢?

再次分析源码,原来仅仅是new ServerSocket(6666);这一步操作就调用了三次Linux API,其对应关系如下图。
Linux API调用

调用栈图示

Socket Bind

同样的,我们可以得出:java中ServerSocket的绑定是调用PlainSocketImpl.socketBind这个native方法来实现的。

源码分析

查看以下JDK源码中PlainSocketImpl.socketBind方法的内容。

@Override
    void socketBind(InetAddress address, int port) throws IOException {
        int nativefd = checkAndReturnNativeFD();

        if (address == null) // ip地址为空则抛出异常
            throw new NullPointerException("inet address argument is null.");

        if (preferIPv4Stack && !(address instanceof Inet4Address)) // 限定IP地址为IPv4版本
            throw new SocketException("Protocol family not supported"); 

        // 调用jvm的bind0方法实现绑定
        bind0(nativefd, address, port, useExclusiveBind); 
        if (port == 0) { // 没有给出端口号
            localport = localPort0(nativefd);
        } else {
            localport = port;
        }

        this.address = address;
    }

可以看到,在上面的方法中通过调用bind0这个方法来实现实现的端口号以及IP地址的绑定。
并且,源码限制目前所支持的IP地址是IPv4版本的(虽然目前IPv4地址已经分配完毕),相信在后续的JDK更新中这里会修改过来。

Socket监听

从之前的Java调用Linux API图中可以看到,在完成Socket的创建和绑定之后,服务器端进入监听的状态,等待客户端发出连接的请求。

调用栈图示

Socket Listen
从上图可以得出:java中ServerSocket的绑定是调用PlainSocketImpl.socketListen这个native方法来实现的。

源码分析

@Override
    void socketListen(int backlog) throws IOException {
        int nativefd = checkAndReturnNativeFD();

        // 调用jvm的listen0方法实现监听
        listen0(nativefd, backlog);
    }

在JDK中监听的实现较为简单,主要是通过调用JVM中listen0来实现的,这里不做过多的展开。

Socket Accept

服务器端一直被动等待着客户端的连接,终于有一个客户端使用与之匹配的IP地址端口号
并在经历了TCP三次握手之后,客户端建立新的连接Socket对象,服务器就与这个客户端建立了TCP连接

调用栈图示

Socket Accept
从上图可以得出:java中ServerSocket的绑定是调用PlainSocketImpl.socketAccept这个native方法来实现的。

源码分析

@Override
    void socketAccept(SocketImpl s) throws IOException {
        int nativefd = checkAndReturnNativeFD();

        if (s == null)
            throw new NullPointerException("socket is null");

        int newfd = -1;
        InetSocketAddress[] isaa = new InetSocketAddress[1];
        if (timeout <= 0) { // 设定有超时计时器
            // 没有超时则调用accept0方法建立连接
            newfd = accept0(nativefd, isaa);
        } else {
            // 否则将该客户端挂入阻塞队列中
            configureBlocking(nativefd, false);
            try {
                waitForNewConnection(nativefd, timeout);
                newfd = accept0(nativefd, isaa);
                if (newfd != -1) {
                    configureBlocking(newfd, true);
                }
            } finally {
                configureBlocking(nativefd, true);
            }
        }

        // 更新socketImpl的文件描述符值
        fdAccess.set(s.fd, newfd);

        // 更新socketImpl中的端口号、ip地址以及localport值
        InetSocketAddress isa = isaa[0];
        s.port = isa.getPort();
        s.address = isa.getAddress();
        s.localport = localport;
        if (preferIPv4Stack && !(s.address instanceof Inet4Address))
            throw new SocketException("Protocol family not supported");
    }

Java Socekt API与Linux Socket API

在上面的调用栈分析中,无论是ServerSocket的创建、绑定、监听,还是连接都伴随着对glibc的调用。

那么glibc到底何许人也?这里引用百度词条的内容:

glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。由于 glibc 囊括了几乎所有的 UNIX 通行的标准,可以想见其内容包罗万象。而就像其他的 UNIX 系统一样,其内含的档案群分散于系统的树状目录结构中,像一个支架一般撑起整个操作系统。在 GNU/Linux 系统中,其C函式库发展史点出了GNU/Linux 演进的几个重要里程碑,用 glibc 作为系统的C函式库,是GNU/Linux演进的一个重要里程碑。

就绑定功能而言,在上述的调用栈追踪中我们知道了所调用的是底层由glibc提供的Bind方法,
但实际上,最终调用内核的SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)。

因此,可以得出结论:

java的socket实现是通过调用操作系统的socket api实现的

参考链接

简单hello/hi程序、分析及Java Socket API与Linux Socket API对比
Linux/Unix socket 基础API (一)
图解Java服务端Socket建立原理
glibc

java实现一个hello/hi的简单的网络聊天程序(代码片段)

使用Java的Socket实现客户端和服务器端之间的连接,实现客户端重复发送数据到服务器端的功能。即,用户可以在控制台不断输入内容,并将内容逐一发送给服务端。并在服务端显示。socket定义    网络上的两个程... 查看详情

使用java实现一个hello/hi的简单的网络聊天程序(代码片段)

1、socket原理    Socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。正如打电话之前,双方必须各自拥有一台电话机一样。     &n... 查看详情

基于python完成一个hello/hi的简单的网络聊天程序(代码片段)

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

一个hello/hi的简单的网络聊天程序(代码片段)

  我选择使用python来实现hello/hi的简单网络聊天程序,源代码包括两个部分,客户端代码和服务器端代码,源代码部分如下图所示:服务器端代码1importsocket23HOST=‘127.0.0.1‘4PORT=888856server=socket.socket()7server.bind((HOST,PORT))8server.list... 查看详情

使用python实现一个hello/hi的简单的网络聊天程序(代码片段)

一、TCP/IP协议通信原理   TCP/IP协议包含的范围非常的广,它是一种四层协议,包含了各种硬件、软件需求的定义。TCP/IP协议确切的说法应该是TCP/UDP/IP协议。UDP协议(UserDatagramProtocol用户数据报协议),是一种保护消息边界的... 查看详情

c语言实现一个hello/hi的简单聊天程序并跟踪分析到系统调用(代码片段)

socket编程介绍Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,可以用它们来开发TCP/IP网络上的应用程序。Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和输出的话,就很容易了解Sock... 查看详情

使用python完成一个hello/hi的简单的网络聊天程序(代码片段)

  在这篇文章中,我将先简要介绍socket原理,然后给出一个利用Python实现的简单通信样例,最后通过跟踪系统调用来分析Python中socket函数与Linux系统调用的对应关系。1.socket简介Socket是应用层与TCP/IP协议族通信的中间软件抽象... 查看详情

一个简单的hello/hi的网络聊天程序(代码片段)

TCP套接字函数了解socket函数为了执行网络I/O,一个进程必须做的第一件事情就是调用socket函数,指定期望的通信协议类型(使用ipv4的TCP、使用ipv6的UDP、Unix域字节流协议等)#include<sys/socket.h>intsocket(intfamily,inttype,intprotocol);返... 查看详情

以您熟悉的编程语言为例完成一个hello/hi的简单的网络聊天程序

在这片博文我们将使用python完成一个hello/hi的简单的网络聊天程序 先做一下准备工作 1.linux的socket基础api:  使用socket()创建套接字intsocket(intaf,inttype,intprotocol);af为IP地址类型,AF_INE和AF_INET6分别对应ipv4和ipv6地址type... 查看详情

用c++完成一个hello/hi的简单的网络聊天程序(代码片段)

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

一个hello/hi的简单的网络聊天程序和pythonsocketapi与linuxsocketapi之间的关系(代码片段)

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

hello/hi的简单的网络聊天程序(代码片段)

一、TCP/IP协议族  要想理解socket首先得熟悉一下TCP/IP协议族, TCP/IP(TransmissionControlProtocol/InternetProtocol)即传输控制协议/网间协议,定义了主机如何连入因特网及数据如何在它们之间传输的标准,从字面意思来看TCP/IP是TCP... 查看详情

在windows环境下用c++完成一个hello/hi网络聊天程序(代码片段)

...络程序编程,接下来,就要学以致用,完成一个hello/hi的网络聊天程序。  Socket介绍      Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模... 查看详情

java案例:基于tcp的简单聊天程序(代码片段)

文章目录一、如何实现TCP通信二、编写C/S架构聊天程序(一)编写服务器端程序-Server.java(二)编写客户端程序-Client.java(三)测试服务器端与客户端能否通信(四)程序优化思路-服务器端采用多... 查看详情

java网络编程基础—基于tcp的nio简单聊天系统

在Java网络编程基础(四)中提到了基于Socket的TCP/IP简单聊天系统实现了一个多客户端之间护法消息的简单聊天系统。其服务端采用了多线程来处理多个客户端的消息发送,并转发给目的用户。但是由于它是基于Socket的,因此是... 查看详情

基于java套接字的简单网络聊天程序

网络中进程之间如何通信本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:消息传递(管道、FIFO、消息队列)同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)共享内存(匿名的和具名的)远程过程... 查看详情

java实现一个简单的网络聊天程序(代码片段)

代码服务器端packagesocket_demo;importjava.io.InputStream;importjava.io.OutputStream;importjava.net.ServerSocket;importjava.net.Socket;publicclassserverpublicstaticvoidmain(String[]args)throwsException//监听 查看详情

项目日志之基于javasocket的网络通讯

...较为方便的编写网络上数据的传递。我们打算通过Java中基于Socket的网络编程实现一个简单的网络通信程序。这就是我们团队项目(开发一款简单的通讯软件,其基本功能是实现一对一的网络信息通讯,并努力向一对多和多对多... 查看详情