【Linux后端服务器开发】socket套接字

目录

一、socket 套接字概述

二、socket 函数接口

三、IP地址与端口号的网络格式

四、TCP协议的本地通信C语言示例


一、socket 套接字概述

socket 是什么?

socket 本质上是一个抽象的概念,它是一组用于网络通信的 API提供了一种统一的接口,使得应用程序可以通过网络进行通信。在不同的操作系统中,socket 的实现方式可能不同,但它们都遵循相同的规范和协议,可以实现跨平台的网络通信

socket 实现通信的原理是基于网络协议栈
当应用程序创建一个 socket 并指定协议族、类型和使用的协议后,操作系统会创建一个对应的套接字,并把它加入到协议栈中。
协议栈是一个由多个层次协议组成的网络协议体系结构,它负责对数据进行封装和解封装,并确保数据能够在网络上正确传输。

当应用程序通过 socket 发送数据时,操作系统会将数据传递给协议栈的上层协议,该协议会对数据进行封装并添加一些必要的信息,例如目标 IP地址和端口号等。然后将封装后的数据传递给下一层协议,直到数据最终被封装成一个网络包并通过网络发送到目标主机。

当目标主机收到网络包后,协议栈会对数据进行解封装,并将数据传递给操作系统中的套接字。如果该套接字是一个监听套接字,操作系统会创建一个新的套接字来处理连接请求,并将新的套接字加入到协议栈中。如果该套接字是一个已连接套接字,操作系统会将数据传递给应用程序处理。

总之,socket 实现通信的原理是基于网络协议栈,通过将数据封装成网络包并通过网络传输,实现了应用程序之间的通信。操作系统负责管理套接字和协议栈,确保数据能够正确传输。

在Linux中,socket是一种文件类型,伪文件,不占用存储空间,可进行IO操作,可间接看做文件描述符使用。

socket 通信流程图:

【Linux后端服务器开发】socket套接字_第1张图片

 

客户端与服务器工作的核心逻辑:

  1. 在客户端向服务器发送请求之前,服务器必须已经初始化完成。
  2. 客户端和服务器的初始化,都需要创建套接字socket,设置IP地址结构体信息。
  3. 服务端在设置完IP地址结构体信息之后,需要bind绑定套接字,通过listen将socket设置为监听状态
  4. 客户端通过connect向服务端发送连接请求,服务端通过accept接收客户端的连接请求,接收成功后获取新的套接字文件描述符。(TCP三次握手)
  5. 客户端发送数据——向文件描述符写入数据write,服务端接收数据——从文件描述符读出数据read,服务端回射数据write,客户端获取回射数据read。
  6. 客户端或服务端发送通信结束信号,close文件描述符,结束通信。(TCP四次挥手)

二、socket 函数接口

socket():创建套接字,返回一个可操作性的文件描述符

int socket(int domain,int type,int protocol);

参数一:表示ip地址类型,常用的有两种

  • 其中AF_INET表示IPv4地址,比如127.0.0.1,这是一个本地 ip
  • 其中AF_INET6表示IPv6地址,比如2001:3CA1:10F:1A:121B:0:0:10

参数二:表示数据传输方式/套接字类型,常见两种

  • SOCK_DGRAM (数据报套接字/无连接的套接字,UDP)
  • SOCK_STREAM(流格式套接字/面向连接的套接字,TCP)

参数三:表示传输协议

  • 理论上前两个参数已经可以推演出采用哪种协议,可以将protocol 的值设为 0,系统自动推演出采用哪种协议

返回值:返回一个套接字(文件描述符fd)

bind():用于服务器,给sockfd套接字绑上本机地址和使用端口,确定了服务器的身份

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

参数一:套接字的fd(文件描述符),socket()函数的返回值

参数二:结构体 ip+port(端口)

参数三:结构体的字节长度

返回值:判断绑定成功失败

listen():用于服务器,使socket处于监听模式,监听时候有客户端连接,并放入队列(同时设置与服务器建立连接的上限)

int listen(int sockfd, int backlog);

参数一:bind绑定ip和端口的套接字

参数二:请求链接客户端队列的最大存放数目

返回值:判断监听成功失败

accept():用于服务器,接收一个客户端的连接请求,并返回连接客户端的套接字便于IO操作,如果没有客户连接会阻塞等待

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

参数一:服务器的套接字(也叫监听套接字),表明了自己的身份

参数二:传出参数,跟我建立连接的客户端的结构体(内含客户端ip+端口)

参数三:结构体长度的指针 &sizeof()

返回值:连接客户端的套接字

connect():用于客户端,向远端服务器发送连接请求

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数一:传入参数,客户端对服务器进行IO操作的文件描述符

参数二:绑定需要连接的服务器的结构体(需要初始化绑上ip和端口),表明目的

参数三:结构体的长度

三、IP地址与端口号的网络格式

【Linux后端服务器开发】socket套接字_第2张图片

 

IP地址

  • IP一般由32位整数组成,按每8位划分为4部分:255.255.255.255 该显示方式为字符串形式,而IP一般是以整数形式显示。
  • 整数IP地址 unsigned int IP_Addr = 1713350848 转化为二进制为:01100110-00011111-10101000-11000000 根据8位划分得到结果为102-31-168-192,由于网络字节倒序的问题,实际IP为192.168.31.102
  • 在网络通信中,我们输入的是字符串风格的IP地址字符串,这时需要我们将IP地址字符串转换成 uint32_t 的类型进行网络通信,建议直接用库函数 inet_addr(const char* ip) 进行转换。
  • 对于服务器而言,bind绑定套接字的时候,IP地址可用 htonl(INADDR_ANY) 进行任意地址绑定。
// 整数风格 uint32_t 转 字符串风格 ip:
// uint32_t ip;
// struct _ip {
//     unsigned char p1;
//     unsigned char p2;
//     unsigned char p3;
//     unsigned char p4;
// };
// std::string strip = to_string(((struct _ip*)&ip)->p1) + to_string(((struct _ip*)&ip)->p2) +
//                     to_string(((struct _ip*)&ip)->p3) + to_string(((struct _ip*)&ip)->p4);
//
// 字符串风格 转 整数风格:
// 整数风格的ip地址存储方式,占用空间更小,网络通信都使用 uint32_t 的ip格式
// 系统提供的转换方式:inet_addr(const char* ip)
// 任意地址绑定:htonl(INADDR_ANY)

端口号port:

  • 端口号port的数据类型是uint16_t,但是在网络通信中的类型是in_port_t,这其实是uint16_t的重命名,但我们还是需要将port端口号从主机格式转换为网络格式
  • 通过 htons(port) 将端口号信息绑定到 struct sockaddr_in 结构体中。

四、TCP协议的本地通信C语言示例

server.c

#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 


int main(int args, char* argv[])
{
    if (args != 2)
    {
        printf("Usage:server port\n");
        exit(1);
    }
    uint16_t port = atoi(argv[1]);  // 启动server的时候指定端口号

    // 1. 创建套接字
    int s_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (s_socket < 0)
        exit(1);

    // 2. 绑定套接字
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);  // Address to accept any incomint message ---> 任意地址绑定
    local.sin_port = htons(port);               // 主机转网络

    if (bind(s_socket, (struct sockaddr*)&local, sizeof(local)) < 0)
        exit(1);
    
    // 3. 监听
    if (listen(s_socket, 5) < 0)
        exit(1);

    // 4. 阻塞等待客户端的连接请求
    struct sockaddr_in peer;
    socklen_t peer_len = sizeof(peer);

    int new_sock = accept(s_socket, (struct sockaddr*)&peer, &peer_len);
    if (new_sock < 0)
        exit(1);

    // 5. 连接成功,面向字节流通信
    while (1)
    {
        char buf[1024];
        int data_len = read(new_sock, buf, sizeof(buf));
        if (data_len == 0)
            break;
        buf[data_len] = 0;
        printf("recv message: %s\n", buf);

        // 6. 发送回射信息(应答数据)
        char out_buf[1024];
        snprintf(out_buf, sizeof(out_buf), "已收到数据: %s\n", buf);
        write(new_sock, out_buf, sizeof(out_buf));
    }

    // 7. 结束连接
    close(s_socket);

    return 0;
}

client.c

#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 

int main(int args, char* argv[])
{
    if (args != 3)
    {
        printf("Usage: client server_ip server_port\n");
        exit(1);
    }
    char* s_ip = argv[1];
    uint16_t s_port = atoi(argv[2]);

    // 1. 创建套接字
    int c_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (c_socket < 0)
        exit(1);

    // client其实也需要bind绑定,不过这一步不需显式绑定(由OS随机指定)

    // 2. 发送连接请求
    struct sockaddr_in server;
    socklen_t s_len = sizeof(server);
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(s_ip);
    server.sin_port = htons(s_port);

    if (connect(c_socket, (struct sockaddr*)&server, s_len) != 0)
    {
        printf("connect fail\n");
        exit(1);
    }
    
    // 3. 建立连接成功,面向字节流通信
    while (1)
    {
        char buf[1024];
        printf("Enter: ");
        gets(buf);
        write(c_socket, buf, sizeof(buf));

        // 4. 获取服务器的应答数据
        char recv_buf[1024];
        int data_len = read(c_socket, recv_buf, sizeof(recv_buf));
        if (data_len == 0)
            break;
        recv_buf[data_len] = 0;
        printf("%s", recv_buf);
    }

    // 5. 结束通信
    close(c_socket);

    return 0;
}

 

先启动服务器,再启动客户端,运行结果:

你可能感兴趣的:(Linux后端服务器开发,linux,服务器,网络,socket,TCP,C语言)