0、阅读本章需要哪些知识

我很少介绍关于基础的东西,因为这些文章太多了,网上关于基础的一抓一大把,可能比我介绍的还好,所以,在阅读本章前,需要具有基本的Socket通信流程、C语法、HTTP请求/响应格式、HTTP响应头字段各代表什么信息,这些就足够了。

一、什么是套接字

关于套接字,多少这里说两句。

套接字其实叫socket,关于socket的文章以前写过一篇,是介绍openjdk下的socket实现,里面说了java中socket底层的的实现方式,但是是在Window环境下,今天在Linux环境下做个演示,以及做个静态资源服务器。

是时候了解Java Socket底层实现了

在插入一个小知识点,Window窗口程序中如何不阻塞主线程实现socket数据读取?并不能使用多线程。

这似乎不好实现,在调用recv去接收数据的时候,一般情况下会阻塞,那么窗口程序就可能发白,那该怎么做呢?

在以前看过这样的一个写法,先不进行recv,而是等待Windows系统主动向程序发送一个消息,这个消息的标识具体已经记不清了,因为过去很多年了,然后在窗口过程中去判断消息,如果是,则在进行recv,这样就不会发生太长时间的阻塞。

简而言之就是单线程实现socket并发,在java中可以使用nio下的包,第一代的api中似乎没办法。

好了在说说什么是socket,有了socket,我们就可以在不同机器上的进程间通过网络进行通信,也可以在同一台机器中不同进程进行通信,通信,就是互相发数据,数据可以是字符,也可以是二进制数据,我们在使用浏览器的时候,浏览器就会和对应的服务器建立一个连接,建立连接后浏览器会构造一个http请求体,然后把数据发送到服务器中,服务器解析http请求体,并处理具体的数据,这些通常是服务器软件来完成的,比如tomcat,如果浏览器要获取一个静态资源,那么tomcat就直接处理了,如果是动态的,那么会交给servlet或jsp处理,处理之后在返回给浏览器。

服务器就需要创建一个称之为ServerSocket的东西(也是一个Socket,服务端的写法和客户端的写法稍微有点不一样),一直等待客户端连接,接下来我们简单写一个ServerSocket,然后通过浏览器去请求资源。

二、ServerSocket创建

下面创建的Socket在收到客户端连接后,读取并发送一串JSON数据,如果我们想源源不断的处理请求,就的写个循环。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

void handlerError(const char *msg) {
    perror(msg);
    exit(1);
}

int main(int argc, char *argv[]) {
    int sockfd;
    socklen_t clilen;
    char buffer[256];
    struct sockaddr_in serv_addrcli_addr;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        handlerError("创建Socket错误");
    bzero((char *) &serv_addr, sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(sockfd, (struct sockaddr *) &serv_addr,
             sizeof(serv_addr)) < 0)
        handlerError("服务绑定失败");
    listen(sockfd, 5);
    clilen = sizeof(cli_addr);
    while (1){
        int clientSocketFd= accept(sockfd,(struct sockaddr *) &cli_addr,&clilen);
        bzero(buffer, 256);
        read(clientSocketFd, buffer, 255);
        printf("客户端消息: %s\n", buffer);
        char* msg ="{\"code\":0,\"msg\":\"ok\"}";
        write(clientSocketFd, msg, strlen(msg));
        shutdown(clientSocketFd,SHUT_WR);
    }
    return 0;
}

每一个函数在下面这篇文章中也说了,就不解释了,虽然是在Window平台,但是都差不多一样

是时候了解Java Socket底层实现了

然后我们启动一个客户进行测试。


public class Main {
    public static void main(String[] args) throws IOException {
        InetSocketAddress inetSocketAddress = new InetSocketAddress(8888);
        SocketChannel socketChannel = SocketChannel.open(inetSocketAddress);

        ByteBuffer src = ByteBuffer.wrap("hello".getBytes());
        /**
         * 发送数据
         */

        int write = socketChannel.write(src);
        /**
         * 读取数据
         */

        ByteBuffer allocate = ByteBuffer.allocate(2048);
        socketChannel.read(allocate);
        allocate.flip();
        byte[] msg = new byte[allocate.limit()];
        allocate.get(msg);
        System.out.println(new String(msg));

    }
}

Linux下C套接字编程,静态资源服务器实现_java

三、HTTP请求体

如果使用浏览器去访问的话,虽然会得到一个错误,但是服务器会收到一大串数据,这就证明了浏览器已经成功建立了一个socket连接,并向服务器发送了http请求体,只是服务端没能按照"约定"返回数据,浏览器没能解析,然后只能报错,通常浏览器会发送两个请求,一个请求是正文,用于访问具体的资源,一个是图标,在请求行行中是以GET /favicon.ico HTTP/1.1这样表示的。

Linux下C套接字编程,静态资源服务器实现_java_02Linux下C套接字编程,静态资源服务器实现_java_03

那么接下来就让服务端按照”约定“返回数据,看看效果。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

void handlerError(const char *msg) {
    perror(msg);
    exit(1);
}

int main(int argc, char *argv[]) {
    int sockfd;
    socklen_t clilen;
    char buffer[256];
    struct sockaddr_in serv_addrcli_addr;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        handlerError("创建Socket错误");
    bzero((char *) &serv_addr, sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(sockfd, (struct sockaddr *) &serv_addr,
             sizeof(serv_addr)) < 0)
        handlerError("服务绑定失败");
    listen(sockfd, 5);
    clilen = sizeof(cli_addr);
    while (1){
        int clientSocketFd= accept(sockfd,(struct sockaddr *) &cli_addr,&clilen);
        bzero(buffer, 256);
        read(clientSocketFd, buffer, 255);
        printf("客户端消息: %s\n", buffer);
        char* msg ="HTTP/1.1 200 OK\n"
                   "Connection: close\n"
                   "Content-Type: text/html\n"
                   "\n"
                   "{\"code\":0,\"msg\":\"ok\"}";
        write(clientSocketFd, msg, strlen(msg));
       shutdown(clientSocketFd,SHUT_WR);

    }
    return 0;
}

再次通过浏览器访问,这会浏览器能成功解析了

Linux下C套接字编程,静态资源服务器实现_java_04

四、传输文件

好了,接下来我们让他支持文件传输,大概分为三步:

  1. 解析HTTP行,取得用户访问的资源名称,这部分在请求行中。
  2. 设置http响应头,就是设置文件名称、文件大小。
  3. 打开对应的文件,读取并发送。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>


int getFileSize(char *filename) {
    FILE *fp = fopen(filename, "r");
    if (!fp) return -1;
    fseek(fp, 0L, SEEK_END);
    int size = ftell(fp);
    fclose(fp);

    return size;
}

char *substring(char *ch, int pos, int length) {
    char *pch = ch;
    char *subch = (char *) calloc(sizeof(char), length + 1);
    int i;
    pch = pch + pos;
    for (i = 0; i < length; i++) {
        subch[i] = *(pch++);
    }
    subch[length] = '\0';
    return subch;
}

char *getRequestRes(char *ch) {
    char *start = strchr(ch, '/') + 1;
    char *end = strchr(start, ' ');
    int startPos = strlen(ch) - strlen(start);
    int resLength = strlen(start) - strlen(end);
    return substring(ch, startPos, resLength);
}

void handlerError(const char *msg) {
    perror(msg);
    exit(1);
}

void *handlerClient(int clientSocketFd) {
    char buffer[2048];
    printf("处理客户端请求%d\n", clientSocketFd);
    bzero(buffer, 2048);
    int sRead =read(clientSocketFd, buffer, 2048);
    if (sRead==-1){return 1;}
    printf("数据%s", buffer);

    char *resName = getRequestRes(buffer);
    char *work = "/home/HouXinLin/test/";
    char *path = (char *) malloc(strlen(resName)+strlen(work));
    strcat(path, work);
    strcat(path, resName);
    int fSize = getFileSize(path);
    printf("路径=%s,大小%d\n", path, fSize);

    /**
     * 构建HTTP相应头
     */

    char *response;
    response = (char *) malloc(400);
    strcat(response, "HTTP/1.1 200 OK\n");
    strcat(response, "Connection: close\nContent-Type: application/octet-stream;\n");
    strcat(response, "Content-Disposition: attachment; filename=");
    strcat(response, resName);
    strcat(response, "\n");
    strcat(response, "Content-Length:");
    sprintf(response + strlen(response), "%d", fSize);
    strcat(response, "\n\n");
    printf("%s\n", response);

    write(clientSocketFd, response, strlen(response));
    free(response);
    int resFD = open(path, O_RDONLY);
    int readBuffer[2048];
    int size = 0;
    /**
     * 发送资源数据
     */

    while ((size = read(resFD, readBuffer, 2048)) > 0) {
        write(clientSocketFd, readBuffer, size);
    }
    close(resFD);
    shutdown(clientSocketFd, 1);
}

int main(int argc, char *argv[]) {

    int sockfd;


    struct sockaddr_in serv_addrcli_addr;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        handlerError("创建Socket错误");
    bzero((char *) &serv_addr, sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);;
    serv_addr.sin_port = htons(8888);
    if (bind(sockfd, (struct sockaddr *) &serv_addr,
             sizeof(serv_addr)) < 0)
        handlerError("服务绑定失败");
    listen(sockfd, 5);

    while (1){
        struct sockaddr_in client_addr;
        char cli_ip[INET_ADDRSTRLEN] = "";
        socklen_t cliaddr_len = sizeof(client_addr);

        printf("等待连接\n");
        int clientSocketFd;
        clientSocketFd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);
        if(clientSocketFd < 0)
        {
            perror("accept");
            continue;
        }

        if (clientSocketFd==-1){continue;}
        struct timeval tv;
        tv.tv_sec = 3;
        tv.tv_usec = 0;
        setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
        handlerClient(clientSocketFd);
    }

    return 0;
}

我们将工作目录设置到了/home/HouXinLin/test下,在这个目录下有这些文件,然后我们试着通过浏览器访问他。

Linux下C套接字编程,静态资源服务器实现_java_05

效果如下:

Linux下C套接字编程,静态资源服务器实现_java_06

这段程序搞了我一天,因为对linux下c编程不算太熟,总结了一句话,c语言真TM难写。