「Socket技术详解」Tinyhttpd

1. Socket常用函数接口及其原理

图解 Socket 函数:

在这里插入图片描述

1.1 使用socket()函数创建套接字

Socket 是应用程序使用网络通信时向系统发送的请求,请求操作系统将网络通信所需的资源。分配给该应用程序返回一个 socket 描述符,随后的连接建立、数据传输等操作都是通过该Socket实现。

函数原型

int socket(int af, int type, int protocol);
  1. af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。大家需要记住 127.0.0.1,它是一个特殊IP地址,表示本机地址,后面的教程会经常用到。
  2. type 为数据传输方式,常用的有 SOCK_STREAM 表示使用面向连接的 TCP 服务,SOCK_DGRAM 表示使用无连接的 UDP 服务。
  3. protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。在确定套接字使用的协议簇和类型时一般设置为 0。

客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态。

1.2 使用bind()和connect()函数

socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理;而客户端要用 connect() 函数建立连接。

函数原型

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  

sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。
下面的代码,将创建的套接字与端口 4000 绑定:

int httpd = 0;
struct sockaddr_in name;
// 创建套接字
httpd = socket(PF_INET, SOCK_STREAM, 0);
if (httpd == -1)
    error_die("socket");
memset(&name, 0, sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(4000);
// INADDR_ANY 0.0.0.0 表示本机的所有 IP
name.sin_addr.s_addr = htonl(INADDR_ANY);
// 将套接字和IP、端口绑定
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
    error_die("bind");

其中 sockaddr_in 结构表示 Socket 套接字地址,包含地址类型、端口号、IP地址、填充字节这4种数据:

  • sin_family 表示地址类型,对于基于TCP/IP传输协议的通信,该值只能是AF_INET;
  • sin_prot 表示端口号,例如:21 或者 80 或者 27015,总之在0 ~ 65535之间;
  • sin_addr 表示32位的IP地址,例如:192.168.1.5 或 202.96.134.133;
  • sin_zero 表示填充字节,一般情况下该值为0;
  • htons()是端口函数,sin_addr 是一个数据结构,包含 s_addr,inet_addr()函数将 IP 转换为长整型数。

connect() 函数用来建立连接,它的原型为:

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); 

1.3 使用listen()和accept()函数

对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。通过 listen() 函数可以让套接字进入被动监听状态,它的原型为:

int listen(int sock, int backlog);

sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求

请求队列:当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。
缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。
如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误

注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); 

它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来

1.4 socket数据的接收和发送

Linux下数据的接收和发送,Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。

两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。

write() 的原型为

ssize_t write(int fd, const void *buf, size_t nbytes);

fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。

read() 的原型为

ssize_t read(int fd, void *buf, size_t nbytes);

fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1

1.5 socket缓冲区以及阻塞模式

socket缓冲区

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区输出缓冲区

write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

在这里插入图片描述

I/O缓冲区特性:

  • I/O缓冲区在每个TCP套接字中单独存在;
  • I/O缓冲区在创建套接字时自动生成;
  • 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  • 关闭套接字将丢失输入缓冲区中的数据。

输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:

unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);
阻塞模式

对于 TCP 套接字(默认情况下),当使用 write()/send() 发送数据时:

  1. 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。
  2. 如果 TCP 协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。
  3. 如果要写入的数据大于缓冲区的最大长度,那么将分批写入
  4. 直到所有数据被写入缓冲区 write()/send() 才能返回。

当使用 read()/recv() 读取数据时:

  1. 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
  2. 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
  3. 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。

这就是 TCP 套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。TCP 套接字默认情况下是阻塞模式.


2. 客户端程序

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int sockfd;
    int len;
    struct sockaddr_in address;
    int result;
    char ch = 'A';

    sockfd = socket(AF_INET, SOCK_STREAM, 0);   // 建立 socket 套接字
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(4000);
    len = sizeof(address);
    result = connect(sockfd, (struct sockaddr *)&address, len);     // 建立连接
    
    if (result == -1)
    {
        perror("oops: client1");
        exit(1);
    }
    write(sockfd, &ch, 1);  // 请求数据
    read(sockfd, &ch, 1);   // 回应数据
    printf("char from server = %c\n", ch);
    close(sockfd);          // 结束连接
    exit(0);
}

3. 服务端程序

3.1 主函数

int main(void)
{
    int server_sock = -1;
    u_short port = 4000;
    int client_sock = -1;
    struct sockaddr_in client_name;
    socklen_t client_name_len = sizeof(client_name);
    pthread_t newthread;

    server_sock = startup(&port);   // 建立 http 连接
    printf("httpd running on port %d\n", port);

    while (1)
    {
        client_sock = accept(server_sock,
                (struct sockaddr *)&client_name,
                &client_name_len);  // 接收客户端请求
        if (client_sock == -1)
            error_die("accept");
        // 建立消息请求队列
        if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
            perror("pthread_create");
    }

    close(server_sock);     // 断开连接

    return(0);
}

3.2 startup()函数

初始化 httpd 服务,包括建立套接字,绑定端口,进行监听等。


持续更新中…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值