实验:通过 Java 模拟 ServerSocket 来调用 OS 底层的 Socket,实现网络通信
目的:了解什么是 natvie 方法和 JNI 调用步骤

Linux 下的 Socket 函数

  1. man 2 socket
  2. NAME
  3. socket - create an endpoint for communication
  4. SYNOPSIS
  5. #include <sys/types.h> /* See NOTES */
  6. #include <sys/socket.h>
  7. int socket(int domain, int type, int protocol);
  8. DESCRIPTION
  9. socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint.
  10. The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently
  11. open for the process.
  12. The domain argument specifies a communication domain; this selects the protocol family which will be used for
  13. communication. These families are defined in <sys/socket.h>. The currently understood formats include:
  14. Name Purpose Man page
  15. AF_UNIX, AF_LOCAL Local communication unix(7)
  16. AF_INET IPv4 Internet protocols ip(7)
  17. AF_INET6 IPv6 Internet protocols ipv6(7)
  18. ... // 省略其他协议
  19. The socket has the indicated type, which specifies the communication semantics. Currently defined types are:
  20. SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data
  21. transmission mechanism may be supported.
  22. SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
  23. ... // 省略其他类型
  24. The protocol specifies a particular protocol to be used with the socket. Normally only a single protocol
  25. exists to support a particular socket type within a given protocol family, in which case protocol can be spec
  26. ified as 0. However, it is possible that many protocols may exist, in which case a particular protocol must
  27. be specified in this manner. The protocol number to use is specific to the communication domain in which
  28. communication is to take place; see protocols(5). See getprotoent(3) on how to map protocol name strings to
  29. protocol numbers.
  30. RETURN VALUE
  31. On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set
  32. appropriately.

Linux 下的 bind 函数

我们可以看出来,如果想要 bind 某一个 socket,需要一个 socket 的文件描述,一个结构体 socketaddr 和地址程度,那么 struct sockaddr 又是在哪里呢?

  1. man 2 bind
  2. NAME
  3. bind - bind a name to a socket
  4. SYNOPSIS
  5. #include <sys/types.h> /* See NOTES */
  6. #include <sys/socket.h>
  7. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  8. DESCRIPTION
  9. When a socket is created with socket(2), it exists in a name space (address family) but has no address
  10. assigned to it. bind() assigns the address specified by addr to the socket referred to by the file descriptor
  11. sockfd. addrlen specifies the size, in bytes, of the address structure pointed to by addr. Traditionally,
  12. this operation is called assigning a name to a socket”.
  13. It is normally necessary to assign a local address using bind() before a SOCK_STREAM socket may receive con
  14. nections (see accept(2)).
  15. The rules used in name binding vary between address families. Consult the manual entries in Section 7 for
  16. detailed information. For AF_INET, see ip(7); for AF_INET6, see ipv6(7); for AF_UNIX, see unix(7); for
  17. AF_APPLETALK, see ddp(7); for AF_PACKET, see packet(7); for AF_X25, see x25(7); and for AF_NETLINK, see
  18. netlink(7).
  19. The actual structure passed for the addr argument will depend on the address family. The sockaddr structure
  20. is defined as something like:
  21. struct sockaddr {
  22. sa_family_t sa_family;
  23. char sa_data[14];
  24. }
  25. The only purpose of this structure is to cast the structure pointer passed in addr in order to avoid compiler
  26. warnings. See EXAMPLE below.
  27. RETURN VALUE
  28. On success, zero is returned. On error, -1 is returned, and errno is set appropriately.


Linux 下的 ip 函数

这里我们可以看到 struct sockaddr_instruct in_addr 结构体的描述

  1. man 7 ip
  2. NAME
  3. ip - Linux IPv4 protocol implementation
  4. SYNOPSIS
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <netinet/ip.h> /* superset of previous */
  8. tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
  9. udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
  10. raw_socket = socket(AF_INET, SOCK_RAW, protocol);
  11. DESCRIPTION
  12. Linux implements the Internet Protocol, version 4, described in RFC 791 and RFC 1122. ip contains a level 2
  13. multicasting implementation conforming to RFC 1112. It also contains an IP router including a packet filter.
  14. The programming interface is BSD-sockets compatible. For more information on sockets, see socket(7).
  15. An IP socket is created using socket(2):
  16. socket(AF_INET, socket_type, protocol);
  17. Valid socket types are SOCK_STREAM to open a tcp(7) socket, SOCK_DGRAM to open a udp(7) socket, or SOCK_RAW to
  18. open a raw(7) socket to access the IP protocol directly. protocol is the IP protocol in the IP header to be
  19. received or sent. The only valid values for protocol are 0 and IPPROTO_TCP for TCP sockets, and 0 and
  20. IPPROTO_UDP for UDP sockets. For SOCK_RAW you may specify a valid IANA IP protocol defined in RFC 1700
  21. assigned numbers.
  22. When a process wants to receive new incoming packets or connections, it should bind a socket to a local inter
  23. face address using bind(2). In this case, only one IP socket may be bound to any given local (address, port)
  24. pair. When INADDR_ANY is specified in the bind call, the socket will be bound to all local interfaces. When
  25. listen(2) is called on an unbound socket, the socket is automatically bound to a random free port with the
  26. local address set to INADDR_ANY. When connect(2) is called on an unbound socket, the socket is automatically
  27. bound to a random free port or to a usable shared port with the local address set to INADDR_ANY.
  28. A TCP local socket address that has been bound is unavailable for some time after closing, unless the
  29. SO_REUSEADDR flag has been set. Care should be taken when using this flag as it makes TCP less reliable.
  30. Address format
  31. An IP socket address is defined as a combination of an IP interface address and a 16-bit port number. The
  32. basic IP protocol does not supply port numbers, they are implemented by higher level protocols like udp(7) and
  33. tcp(7). On raw sockets sin_port is set to the IP protocol.
  34. struct sockaddr_in {
  35. sa_family_t sin_family; /* address family: AF_INET */
  36. in_port_t sin_port; /* port in network byte order */
  37. struct in_addr sin_addr; /* internet address */
  38. };
  39. /* Internet address. */
  40. struct in_addr {
  41. uint32_t s_addr; /* address in network byte order */
  42. };
  43. sin_family is always set to AF_INET. This is required; in Linux 2.2 most networking functions return EINVAL
  44. when this setting is missing. sin_port contains the port in network byte order. The port numbers below 1024
  45. are called privileged ports (or sometimes: reserved ports). Only a privileged process (on Linux: a process
  46. that has the CAP_NET_BIND_SERVICE capability in the user namespace governing its network namespace) may
  47. };
  48. /* Internet address. */
  49. struct in_addr {
  50. uint32_t s_addr; /* address in network byte order */
  51. };
  52. sin_family is always set to AF_INET. This is required; in Linux 2.2 most networking functions return EINVAL
  53. when this setting is missing. sin_port contains the port in network byte order. The port numbers below 1024
  54. are called privileged ports (or sometimes: reserved ports). Only a privileged process (on Linux: a process
  55. that has the CAP_NET_BIND_SERVICE capability in the user namespace governing its network namespace) may
  56. bind(2) to these sockets. Note that the raw IPv4 protocol as such has no concept of a port, they are imple
  57. mented only by higher protocols like tcp(7) and udp(7).
  58. sin_addr is the IP host address. The s_addr member of struct in_addr contains the host interface address in
  59. network byte order. in_addr should be assigned one of the INADDR_* values (e.g., INADDR_LOOPBACK) using
  60. htonl(3) or set using the inet_aton(3), inet_addr(3), inet_makeaddr(3) library functions or directly with the
  61. name resolver (see gethostbyname(3)).
  62. IPv4 addresses are divided into unicast, broadcast, and multicast addresses. Unicast addresses specify a sin
  63. gle interface of a host, broadcast addresses specify all hosts on a network, and multicast addresses address
  64. all hosts in a multicast group. Datagrams to broadcast addresses can be sent or received only when the
  65. SO_BROADCAST socket flag is set. In the current implementation, connection-oriented sockets are allowed to
  66. use only unicast addresses.
  67. Note that the address and the port are always stored in network byte order. In particular, this means that
  68. you need to call htons(3) on the number that is assigned to a port. All address/port manipulation functions
  69. in the standard library work in network byte order.
  70. There are several special addresses: INADDR_LOOPBACK (127.0.0.1) always refers to the local host via the loop
  71. back device; INADDR_ANY (0.0.0.0) means any address for binding; INADDR_BROADCAST (255.255.255.255) means any
  72. host and has the same effect on bind as INADDR_ANY for historical reasons.


Linux 下的 accept 函数

  1. man 2 accept
  2. NAME
  3. accept, accept4 - accept a connection on a socket
  4. SYNOPSIS
  5. #include <sys/types.h> /* See NOTES */
  6. #include <sys/socket.h>
  7. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  8. #define _GNU_SOURCE /* See feature_test_macros(7) */
  9. #include <sys/socket.h>
  10. int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
  11. DESCRIPTION
  12. The accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET). It
  13. extracts the first connection request on the queue of pending connections for the listening socket, sockfd,
  14. creates a new connected socket, and returns a new file descriptor referring to that socket. The newly created
  15. socket is not in the listening state. The original socket sockfd is unaffected by this call.
  16. The argument sockfd is a socket that has been created with socket(2), bound to a local address with bind(2),
  17. and is listening for connections after a listen(2).
  18. The argument addr is a pointer to a sockaddr structure. This structure is filled in with the address of the
  19. peer socket, as known to the communications layer. The exact format of the address returned addr is deter
  20. mined by the socket's address family (see socket(2) and the respective protocol man pages). When addr is
  21. NULL, nothing is filled in; in this case, addrlen is not used, and should also be NULL.
  22. The addrlen argument is a value-result argument: the caller must initialize it to contain the size (in bytes)
  23. of the structure pointed to by addr; on return it will contain the actual size of the peer address.
  24. The returned address is truncated if the buffer provided is too small; in this case, addrlen will return a
  25. value greater than was supplied to the call.
  26. If no pending connections are present on the queue, and the socket is not marked as nonblocking, accept()
  27. blocks the caller until a connection is present. If the socket is marked nonblocking and no pending connec‐
  28. tions are present on the queue, accept() fails with the error EAGAIN or EWOULDBLOCK.
  29. In order to be notified of incoming connections on a socket, you can use select(2), poll(2), or epoll(7). A
  30. readable event will be delivered when a new connection is attempted and you may then call accept() to get a
  31. socket for that connection. Alternatively, you can set the socket to deliver SIGIO when activity occurs on a
  32. socket; see socket(7) for details.
  33. If flags is 0, then accept4() is the same as accept(). The following values can be bitwise ORed in flags to
  34. obtain different behavior:
  35. SOCK_NONBLOCK Set the O_NONBLOCK file status flag on the new open file description. Using this flag saves
  36. extra calls to fcntl(2) to achieve the same result.
  37. SOCK_CLOEXEC Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of
  38. the O_CLOEXEC flag in open(2) for reasons why this may be useful.
  39. RETURN VALUE
  40. On success, these system calls return a nonnegative integer that is a file descriptor for the accepted socket.
  41. On error, -1 is returned, and errno is set appropriately.


Linux 下的 read 函数

  1. NAME
  2. read - read from a file descriptor
  3. SYNOPSIS
  4. #include <unistd.h>
  5. ssize_t read(int fd, void *buf, size_t count);
  6. DESCRIPTION
  7. read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.
  8. On files that support seeking, the read operation commences at the file offset, and the file offset is incre
  9. mented by the number of bytes read. If the file offset is at or past the end of file, no bytes are read, and
  10. read() returns zero.
  11. If count is zero, read() may detect the errors described below. In the absence of any errors, or if read()
  12. does not check for errors, a read() with a count of 0 returns zero and has no other effects.
  13. According to POSIX.1, if count is greater than SSIZE_MAX, the result is implementation-defined; see NOTES for
  14. the upper limit on Linux.
  15. RETURN VALUE
  16. On success, the number of bytes read is returned (zero indicates end of file), and the file position is
  17. advanced by this number. It is not an error if this number is smaller than the number of bytes requested;
  18. this may happen for example because fewer bytes are actually available right now (maybe because we were close
  19. to end-of-file, or because we are reading from a pipe, or from a terminal), or because read() was interrupted
  20. by a signal. See also NOTES.
  21. On error, -1 is returned, and errno is set appropriately. In this case, it is left unspecified whether the
  22. file position (if any) changes.


C 语言启动 socket 主要步骤

此时我们就可以用 C 语言来调用 Linux 下的 Socket 函数了

C 语言调用 socket 函数

  1. // AF_INET: IPV4
  2. // SOCK_STREAM: TCP/IP
  3. // 0: 默认为 0
  4. // 返回值 lfd: 返回一个文件描述符
  5. int lfd = socket(AF_INET, SOCK_STREAM, 0);


C 语言调用 bind 函数

  1. struct sockaddr_in my_addr; // 定义一个结构体
  2. my_addr.sin_family = AF_INET; // IPV4 协议
  3. my_addr.sin_port = htons(8080); // 整型变量从主机字节顺序转变成网络字节顺序
  4. my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // htonl:将主机数转换成无符号长整型的网络字节顺序
  5. bind(lfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
  6. listen(lfd, 128); // 监听此 socket 在同一时刻的连接数(并发数)

调用完 bind 函数,理论上我们就可以进行 accept 等待线程接入了

C 语言调用 accept 函数

  1. struct sockaddr_in client_addr;
  2. char client_ip[INET_ADDRSTRLEN] = "";
  3. socklen_t client_addr_len = sizeof(client_addr);
  4. int connfd = accept(lfd, (struct sockaddr*)&client_addr, &client_addr_len);

当调用完 accept 函数后,就可以获取到客户端的文件描述符,然后调用 read 函数就可以获取客户端发送的数据

C 语言调用 read 函数

  1. char recv_buf[512] = "";
  2. while(1) {
  3. // ssize_t read(int fd, void *buf, size_t count);
  4. int length = read(connfd, recv_buf, sizeof(recv_buf)); // 读取流中的内容到缓冲区
  5. printf("recv data=%d\n", length);
  6. printf("%s\n", recv_buf);
  7. }

完整的 C 文件内容

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <arpa/inet.h>
  8. #include <sys/types.h>
  9. #include "WeServerSocket.h"
  10. JNIEXPORT void JNICALL Java_WeServerSocket_conn(JNIEnv *env, jobject cl, int jint) {
  11. // int socket(int domain, int type, int protocol);
  12. int lfd = socket(AF_INET, SOCK_STREAM, 0);
  13. // int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  14. struct sockaddr_in my_addr; // 定义一个结构体
  15. my_addr.sin_family = AF_INET; // IPV4 协议
  16. my_addr.sin_port = htons(jint); // 整型变量从主机字节顺序转变成网络字节顺序
  17. my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // htonl:将主机数转换成无符号长整型的网络字节顺序
  18. bind(lfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
  19. // int listen(int sockfd, int backlog);
  20. listen(lfd, 128); // 监听此 socket 在同一时刻的连接数(并发数)
  21. printf("-listen client @port=%d...\n", jint);
  22. // int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  23. struct sockaddr_in client_addr;
  24. char client_ip[INET_ADDRSTRLEN] = ""; // 定义一个 client_ip 数组,INET_ADDRSTRLEN = 16
  25. socklen_t client_addr_len = sizeof(client_addr);
  26. int connfd = accept(lfd, (struct sockaddr*)&client_addr, &client_addr_len); // 返回客户端 socket 的文件描述符
  27. // const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  28. inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); // 将客户端地址(网络字节)转化为数值,放到 client_ip 数组中
  29. printf("------------------------------------------\n");
  30. printf("client ip=%s, port=%d\n", client_ip, ntohs(client_addr.sin_port)); // ntohs:将一个16位数由网络字节顺序转换为主机字节顺序
  31. char recv_buf[512] = "";
  32. while(1) {
  33. // ssize_t read(int fd, void *buf, size_t count);
  34. int length = read(connfd, recv_buf, sizeof(recv_buf)); // 读取流中的内容到缓冲区
  35. printf("recv data=%d\n", length);
  36. printf("%s\n", recv_buf);
  37. }
  38. close(connfd);
  39. printf("client closed\n");
  40. close(lfd);
  41. }

JNI 调用步骤

  1. Java 调用 native 方法

    1. public class WeServerSocket {
    2. static {
    3. System.loadLibrary("WeNativeNet");
    4. }
    5. public static void main(String[] args) throws IOException {
    6. WeServerSocket serverSocket = new WeServerSocket();
    7. serverSocket.conn(8080);
    8. }
    9. public native void conn(int port);
    10. }
  2. 装载库,保证 JVM 在启动的时候就会装载所需要的库(so 文件),故而一般都是 static 方法

    1. static {
    2. System.loadLibrary("WeNativeNet");
    3. }
  3. 编译成 class 文件

    1. javac WeServerSocket.java
  4. 生成 .h 的头文件

    1. javah 包名.类名
    2. javah WeServerSocket
  5. 将 .h 头文件包含到 c 文件中,并查看 .h 头文件中的方法签名信息,复制到 c 文件中 ```bash

    include “WeServerSocket.h”

JNIEXPORT void JNICALL Java_WeServerSocket_conn(JNIEnv *env, jobject cl, int jint)

  1. 6. C 文件编译动态连接库 .so 文件
  2. - /usr/lib/jdk/jdk1.8.0_231/include 目录下包括 jni.h 文件
  3. - /usr/lib/jdk/jdk1.8.0_231/include/linux 目录下包括 md_jni.h 文件
  4. - 动态库前面必须是 lib 开头
  5. ```bash
  6. gcc -fPIC -I /usr/lib/jdk/jdk1.8.0_231/include -I /usr/lib/jdk/jdk1.8.0_231/include/linux -shared -o lib动态库名.so C文件名.c -pthread
  1. 把这个动态连接库加入到 path 环境变量下(临时),如果需要的话可以加到 /etc/profile 下

    1. export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/
  2. 运行 class 文件 ```bash java WeServerSocket

-listen client @port=8080…

  1. 9. nc 测试
  2. ```bash
  3. nc 127.0.0.1 8080
  4. hello server

至此完成了模拟 ServerSocket 启动 socket 服务端的所有流程,同时你也清楚了 natvie 方法和 JNI 调用方式