实验:通过 Java 模拟 ServerSocket 来调用 OS 底层的 Socket,实现网络通信
目的:了解什么是 natvie 方法和 JNI 调用步骤
Linux 下的 Socket 函数
man 2 socketNAMEsocket - create an endpoint for communicationSYNOPSIS#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int socket(int domain, int type, int protocol);DESCRIPTIONsocket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint.The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currentlyopen for the process.The domain argument specifies a communication domain; this selects the protocol family which will be used forcommunication. These families are defined in <sys/socket.h>. The currently understood formats include:Name Purpose Man pageAF_UNIX, AF_LOCAL Local communication unix(7)AF_INET IPv4 Internet protocols ip(7)AF_INET6 IPv6 Internet protocols ipv6(7)... // 省略其他协议The socket has the indicated type, which specifies the communication semantics. Currently defined types are:SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band datatransmission mechanism may be supported.SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).... // 省略其他类型The protocol specifies a particular protocol to be used with the socket. Normally only a single protocolexists to support a particular socket type within a given protocol family, in which case protocol can be spec‐ified as 0. However, it is possible that many protocols may exist, in which case a particular protocol mustbe specified in this manner. The protocol number to use is specific to the “communication domain” in whichcommunication is to take place; see protocols(5). See getprotoent(3) on how to map protocol name strings toprotocol numbers.RETURN VALUEOn success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is setappropriately.
Linux 下的 bind 函数
我们可以看出来,如果想要 bind 某一个 socket,需要一个 socket 的文件描述,一个结构体 socketaddr 和地址程度,那么 struct sockaddr 又是在哪里呢?
man 2 bindNAMEbind - bind a name to a socketSYNOPSIS#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);DESCRIPTIONWhen a socket is created with socket(2), it exists in a name space (address family) but has no addressassigned to it. bind() assigns the address specified by addr to the socket referred to by the file descriptorsockfd. addrlen specifies the size, in bytes, of the address structure pointed to by addr. Traditionally,this operation is called “assigning a name to a socket”.It is normally necessary to assign a local address using bind() before a SOCK_STREAM socket may receive con‐nections (see accept(2)).The rules used in name binding vary between address families. Consult the manual entries in Section 7 fordetailed information. For AF_INET, see ip(7); for AF_INET6, see ipv6(7); for AF_UNIX, see unix(7); forAF_APPLETALK, see ddp(7); for AF_PACKET, see packet(7); for AF_X25, see x25(7); and for AF_NETLINK, seenetlink(7).The actual structure passed for the addr argument will depend on the address family. The sockaddr structureis defined as something like:struct sockaddr {sa_family_t sa_family;char sa_data[14];}The only purpose of this structure is to cast the structure pointer passed in addr in order to avoid compilerwarnings. See EXAMPLE below.RETURN VALUEOn success, zero is returned. On error, -1 is returned, and errno is set appropriately.
Linux 下的 ip 函数
这里我们可以看到 struct sockaddr_in 与 struct in_addr 结构体的描述
man 7 ipNAMEip - Linux IPv4 protocol implementationSYNOPSIS#include <sys/socket.h>#include <netinet/in.h>#include <netinet/ip.h> /* superset of previous */tcp_socket = socket(AF_INET, SOCK_STREAM, 0);udp_socket = socket(AF_INET, SOCK_DGRAM, 0);raw_socket = socket(AF_INET, SOCK_RAW, protocol);DESCRIPTIONLinux implements the Internet Protocol, version 4, described in RFC 791 and RFC 1122. ip contains a level 2multicasting implementation conforming to RFC 1112. It also contains an IP router including a packet filter.The programming interface is BSD-sockets compatible. For more information on sockets, see socket(7).An IP socket is created using socket(2):socket(AF_INET, socket_type, protocol);Valid socket types are SOCK_STREAM to open a tcp(7) socket, SOCK_DGRAM to open a udp(7) socket, or SOCK_RAW toopen a raw(7) socket to access the IP protocol directly. protocol is the IP protocol in the IP header to bereceived or sent. The only valid values for protocol are 0 and IPPROTO_TCP for TCP sockets, and 0 andIPPROTO_UDP for UDP sockets. For SOCK_RAW you may specify a valid IANA IP protocol defined in RFC 1700assigned numbers.When a process wants to receive new incoming packets or connections, it should bind a socket to a local inter‐face address using bind(2). In this case, only one IP socket may be bound to any given local (address, port)pair. When INADDR_ANY is specified in the bind call, the socket will be bound to all local interfaces. Whenlisten(2) is called on an unbound socket, the socket is automatically bound to a random free port with thelocal address set to INADDR_ANY. When connect(2) is called on an unbound socket, the socket is automaticallybound to a random free port or to a usable shared port with the local address set to INADDR_ANY.A TCP local socket address that has been bound is unavailable for some time after closing, unless theSO_REUSEADDR flag has been set. Care should be taken when using this flag as it makes TCP less reliable.Address formatAn IP socket address is defined as a combination of an IP interface address and a 16-bit port number. Thebasic IP protocol does not supply port numbers, they are implemented by higher level protocols like udp(7) andtcp(7). On raw sockets sin_port is set to the IP protocol.struct sockaddr_in {sa_family_t sin_family; /* address family: AF_INET */in_port_t sin_port; /* port in network byte order */struct in_addr sin_addr; /* internet address */};/* Internet address. */struct in_addr {uint32_t s_addr; /* address in network byte order */};sin_family is always set to AF_INET. This is required; in Linux 2.2 most networking functions return EINVALwhen this setting is missing. sin_port contains the port in network byte order. The port numbers below 1024are called privileged ports (or sometimes: reserved ports). Only a privileged process (on Linux: a processthat has the CAP_NET_BIND_SERVICE capability in the user namespace governing its network namespace) may};/* Internet address. */struct in_addr {uint32_t s_addr; /* address in network byte order */};sin_family is always set to AF_INET. This is required; in Linux 2.2 most networking functions return EINVALwhen this setting is missing. sin_port contains the port in network byte order. The port numbers below 1024are called privileged ports (or sometimes: reserved ports). Only a privileged process (on Linux: a processthat has the CAP_NET_BIND_SERVICE capability in the user namespace governing its network namespace) maybind(2) to these sockets. Note that the raw IPv4 protocol as such has no concept of a port, they are imple‐mented only by higher protocols like tcp(7) and udp(7).sin_addr is the IP host address. The s_addr member of struct in_addr contains the host interface address innetwork byte order. in_addr should be assigned one of the INADDR_* values (e.g., INADDR_LOOPBACK) usinghtonl(3) or set using the inet_aton(3), inet_addr(3), inet_makeaddr(3) library functions or directly with thename resolver (see gethostbyname(3)).IPv4 addresses are divided into unicast, broadcast, and multicast addresses. Unicast addresses specify a sin‐gle interface of a host, broadcast addresses specify all hosts on a network, and multicast addresses addressall hosts in a multicast group. Datagrams to broadcast addresses can be sent or received only when theSO_BROADCAST socket flag is set. In the current implementation, connection-oriented sockets are allowed touse only unicast addresses.Note that the address and the port are always stored in network byte order. In particular, this means thatyou need to call htons(3) on the number that is assigned to a port. All address/port manipulation functionsin the standard library work in network byte order.There are several special addresses: INADDR_LOOPBACK (127.0.0.1) always refers to the local host via the loop‐back device; INADDR_ANY (0.0.0.0) means any address for binding; INADDR_BROADCAST (255.255.255.255) means anyhost and has the same effect on bind as INADDR_ANY for historical reasons.
Linux 下的 accept 函数
man 2 acceptNAMEaccept, accept4 - accept a connection on a socketSYNOPSIS#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);#define _GNU_SOURCE /* See feature_test_macros(7) */#include <sys/socket.h>int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);DESCRIPTIONThe accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET). Itextracts the first connection request on the queue of pending connections for the listening socket, sockfd,creates a new connected socket, and returns a new file descriptor referring to that socket. The newly createdsocket is not in the listening state. The original socket sockfd is unaffected by this call.The argument sockfd is a socket that has been created with socket(2), bound to a local address with bind(2),and is listening for connections after a listen(2).The argument addr is a pointer to a sockaddr structure. This structure is filled in with the address of thepeer socket, as known to the communications layer. The exact format of the address returned addr is deter‐mined by the socket's address family (see socket(2) and the respective protocol man pages). When addr isNULL, nothing is filled in; in this case, addrlen is not used, and should also be NULL.The addrlen argument is a value-result argument: the caller must initialize it to contain the size (in bytes)of the structure pointed to by addr; on return it will contain the actual size of the peer address.The returned address is truncated if the buffer provided is too small; in this case, addrlen will return avalue greater than was supplied to the call.If no pending connections are present on the queue, and the socket is not marked as nonblocking, accept()blocks the caller until a connection is present. If the socket is marked nonblocking and no pending connec‐tions are present on the queue, accept() fails with the error EAGAIN or EWOULDBLOCK.In order to be notified of incoming connections on a socket, you can use select(2), poll(2), or epoll(7). Areadable event will be delivered when a new connection is attempted and you may then call accept() to get asocket for that connection. Alternatively, you can set the socket to deliver SIGIO when activity occurs on asocket; see socket(7) for details.If flags is 0, then accept4() is the same as accept(). The following values can be bitwise ORed in flags toobtain different behavior:SOCK_NONBLOCK Set the O_NONBLOCK file status flag on the new open file description. Using this flag savesextra calls to fcntl(2) to achieve the same result.SOCK_CLOEXEC Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description ofthe O_CLOEXEC flag in open(2) for reasons why this may be useful.RETURN VALUEOn success, these system calls return a nonnegative integer that is a file descriptor for the accepted socket.On error, -1 is returned, and errno is set appropriately.
Linux 下的 read 函数
NAMEread - read from a file descriptorSYNOPSIS#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);DESCRIPTIONread() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.On files that support seeking, the read operation commences at the file offset, and the file offset is incre‐mented by the number of bytes read. If the file offset is at or past the end of file, no bytes are read, andread() returns zero.If count is zero, read() may detect the errors described below. In the absence of any errors, or if read()does not check for errors, a read() with a count of 0 returns zero and has no other effects.According to POSIX.1, if count is greater than SSIZE_MAX, the result is implementation-defined; see NOTES forthe upper limit on Linux.RETURN VALUEOn success, the number of bytes read is returned (zero indicates end of file), and the file position isadvanced by this number. It is not an error if this number is smaller than the number of bytes requested;this may happen for example because fewer bytes are actually available right now (maybe because we were closeto end-of-file, or because we are reading from a pipe, or from a terminal), or because read() was interruptedby a signal. See also NOTES.On error, -1 is returned, and errno is set appropriately. In this case, it is left unspecified whether thefile position (if any) changes.
C 语言启动 socket 主要步骤
此时我们就可以用 C 语言来调用 Linux 下的 Socket 函数了
C 语言调用 socket 函数
// AF_INET: IPV4// SOCK_STREAM: TCP/IP// 0: 默认为 0// 返回值 lfd: 返回一个文件描述符int lfd = socket(AF_INET, SOCK_STREAM, 0);
C 语言调用 bind 函数
struct sockaddr_in my_addr; // 定义一个结构体my_addr.sin_family = AF_INET; // IPV4 协议my_addr.sin_port = htons(8080); // 整型变量从主机字节顺序转变成网络字节顺序my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // htonl:将主机数转换成无符号长整型的网络字节顺序bind(lfd, (struct sockaddr*)&my_addr, sizeof(my_addr));listen(lfd, 128); // 监听此 socket 在同一时刻的连接数(并发数)
调用完 bind 函数,理论上我们就可以进行 accept 等待线程接入了
C 语言调用 accept 函数
struct sockaddr_in client_addr;char client_ip[INET_ADDRSTRLEN] = "";socklen_t client_addr_len = sizeof(client_addr);int connfd = accept(lfd, (struct sockaddr*)&client_addr, &client_addr_len);
当调用完 accept 函数后,就可以获取到客户端的文件描述符,然后调用 read 函数就可以获取客户端发送的数据
C 语言调用 read 函数
char recv_buf[512] = "";while(1) {// ssize_t read(int fd, void *buf, size_t count);int length = read(connfd, recv_buf, sizeof(recv_buf)); // 读取流中的内容到缓冲区printf("recv data=%d\n", length);printf("%s\n", recv_buf);}
完整的 C 文件内容
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <sys/types.h>#include "WeServerSocket.h"JNIEXPORT void JNICALL Java_WeServerSocket_conn(JNIEnv *env, jobject cl, int jint) {// int socket(int domain, int type, int protocol);int lfd = socket(AF_INET, SOCK_STREAM, 0);// int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);struct sockaddr_in my_addr; // 定义一个结构体my_addr.sin_family = AF_INET; // IPV4 协议my_addr.sin_port = htons(jint); // 整型变量从主机字节顺序转变成网络字节顺序my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // htonl:将主机数转换成无符号长整型的网络字节顺序bind(lfd, (struct sockaddr*)&my_addr, sizeof(my_addr));// int listen(int sockfd, int backlog);listen(lfd, 128); // 监听此 socket 在同一时刻的连接数(并发数)printf("-listen client @port=%d...\n", jint);// int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);struct sockaddr_in client_addr;char client_ip[INET_ADDRSTRLEN] = ""; // 定义一个 client_ip 数组,INET_ADDRSTRLEN = 16socklen_t client_addr_len = sizeof(client_addr);int connfd = accept(lfd, (struct sockaddr*)&client_addr, &client_addr_len); // 返回客户端 socket 的文件描述符// const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); // 将客户端地址(网络字节)转化为数值,放到 client_ip 数组中printf("------------------------------------------\n");printf("client ip=%s, port=%d\n", client_ip, ntohs(client_addr.sin_port)); // ntohs:将一个16位数由网络字节顺序转换为主机字节顺序char recv_buf[512] = "";while(1) {// ssize_t read(int fd, void *buf, size_t count);int length = read(connfd, recv_buf, sizeof(recv_buf)); // 读取流中的内容到缓冲区printf("recv data=%d\n", length);printf("%s\n", recv_buf);}close(connfd);printf("client closed\n");close(lfd);}
JNI 调用步骤
Java 调用 native 方法
public class WeServerSocket {static {System.loadLibrary("WeNativeNet");}public static void main(String[] args) throws IOException {WeServerSocket serverSocket = new WeServerSocket();serverSocket.conn(8080);}public native void conn(int port);}
装载库,保证 JVM 在启动的时候就会装载所需要的库(so 文件),故而一般都是 static 方法
static {System.loadLibrary("WeNativeNet");}
编译成 class 文件
javac WeServerSocket.java
生成 .h 的头文件
javah 包名.类名javah WeServerSocket
将 .h 头文件包含到 c 文件中,并查看 .h 头文件中的方法签名信息,复制到 c 文件中 ```bash
include “WeServerSocket.h”
JNIEXPORT void JNICALL Java_WeServerSocket_conn(JNIEnv *env, jobject cl, int jint)
6. 将 C 文件编译动态连接库 .so 文件- /usr/lib/jdk/jdk1.8.0_231/include 目录下包括 jni.h 文件- /usr/lib/jdk/jdk1.8.0_231/include/linux 目录下包括 md_jni.h 文件- 动态库前面必须是 lib 开头```bashgcc -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
把这个动态连接库加入到 path 环境变量下(临时),如果需要的话可以加到 /etc/profile 下
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/
运行 class 文件 ```bash java WeServerSocket
-listen client @port=8080…
9. 用 nc 测试```bashnc 127.0.0.1 8080hello server
至此完成了模拟 ServerSocket 启动 socket 服务端的所有流程,同时你也清楚了 natvie 方法和 JNI 调用方式
