Linux socket编程(2):socket函数介绍及C/S模型代码实现

上一节简单介绍了一下套接字、字节序和地址结构体的概念,算是对socket有一个入门的了解。这一节就实现一个客户端-服务端的代码,从这个例子中来学习socket函数的使用。

1 客户端/服务端模型

在客户端-服务器模型中,客户端和服务器之间的协作和通信是通过网络实现的,允许用户从远程位置访问和利用服务器上的资源和服务。这种模型的广泛应用使得它成为网络应用程序设计的基本框架之一。对于Linux的socket来说,建立通信的整体流程如下:

在这里插入图片描述

2 套接字函数

首先先来学习一下前面的流程图中出现的一些套接字函数

2.1 socket:创建套接字

我们可以使用socket函数创建一个套接字,套接字可以是不同类型,例如TCP套接字或UDP套接字。

int socket(int domain, int type, int protocol);
  1. domain(地址族,Address Family):常用的有AF_INET(IPv4地址族)、AF_INET6(IPv6地址族)和AF_UNIX(Unix域套接字,用于本地进程之间的通信)
  2. type(套接字类型,Socket Type),常用的有:
    • SOCK_STREAM:流式套接字,也称为面向连接的套接字。用于可靠的、面向连接的数据传输,例如TCP。
    • SOCK_DGRAM:数据报套接字,也称为无连接的套接字。用于无连接、不可靠的数据传输,例如UDP。
  3. protocol(协议):设置0时系统会自动选择适当的协议,例如,创建TCP套接字会使用IPPROTO_TCP协议。

2.2 bind:绑定套接字

bind函数用于将一个套接字绑定到一个特定的IP地址和端口号,以便套接字可以在该地址上接受传入的连接请求或数据包。这是在创建服务器程序时常用的操作,因为服务器通常需要监听特定的地址和端口以等待客户端的连接。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:要绑定的套接字描述符
  • addr:指向一个struct sockaddr类型的结构体,其中包含了要绑定的IP地址和端口信息。在上一篇文章中有介绍,这个结构体可以是通用的,也可以是特定的(如IPV4、IPV6特定的)
  • addrlenaddr结构体的长度,通常使用sizeof(struct sockaddr)来获取

2.3 listen:监听套接字

listen函数用于将服务器套接字(通常是TCP套接字)设置为监听状态,以便它可以接受客户端的连接请求。listen函数通常与bindaccept函数一起使用,以创建一个典型的服务器程序,用于等待客户端连接。

int listen(int sockfd, int backlog);
  • sockfd:要监听的套接字描述符,通常是一个已经通过bind绑定到特定地址和端口的套接字

  • backlog:指定在等待队列中可以排队等待的连接请求的最大数量,即服务器可以同时处理的连接请求的数量

SOMAXCONN 可以作为backlog的参数,它是一个系统常量,表示系统所支持的最大队列长度。SOMAXCONN 的具体值在内核编译时就已经指定,通常是一个相对较大的正整数。使用 SOMAXCONN 可以确保使用系统支持的最大队列长度,以应对高负载的服务器程序。

在这里插入图片描述

如果用户设置的backlog大于SOMAXCONN,则backlog就会设置为SOMAXCONN

2.4 accpet:接受客户端连接请求

accept 函数用于在服务器端套接字(通常是被动套接字)上接受客户端的连接请求,创建一个新的套接字,并在新的套接字上与客户端进行通信。


主动套接字和被动套接字?

主动套接字是客户端套接字,使用 connect 主动连接到服务器。被动套接字是服务器套接字,使用 accept 来接受客户端的连接请求。


accept 是套接字编程中非常重要的函数之一,用于建立服务器与客户端之间的通信通道。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:要接受连接请求的套接字描述符,通常是服务器端套接字,即被动套接字
  • addr:一个指向struct sockaddr类型的结构体指针,用于存储客户端的地址信息(IP地址和端口号)。传入 NULL表示不关心客户端地址信息。
  • addrlen:一个指向socklen_t类型的指针,用于传入addr结构体的长度。通常,可以传入NULL

2.5 connect:与服务器套接字建立连接

connect函数用于客户端套接字建立与服务器套接字的连接。它是网络编程中非常重要的函数,用于建立通信链路,以便客户端可以与服务器进行数据传输。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:要连接的客户端套接字的描述符
  • addr:指向一个struct sockaddr类型的结构体指针,其中包含了服务器的地址信息,包括 IP 地址和端口号。这个结构体的类型(AF_INETAF_INET6 等)应与客户端套接字的类型相匹配。
  • addrlenaddr 结构体的长度。

2.6 close:关闭套接字

当不再需要使用套接字时,使用close函数可以释放相关资源,包括文件描述符和系统内核资源。

int close(int sockfd);
  • sockfd:要关闭的套接字的文件描述符。

2.7 write和send:写数据

write函数:通用的文件写入函数,可以用于向文件描述符写入数据,也可以用于套接字。

ssize_t write(int fd, const void *buf, size_t count);
  • fd 是文件描述符,可以是套接字描述符,也可以是其他文件描述符。
  • buf 是包含要写入数据的缓冲区的指针。
  • count 是要写入的数据字节数。
  • 返回值是发送成功的字节数,如果发送失败则返回-1,并使用errno设置错误代码。

send函数:专门用于套接字通信的函数,它提供了更多的选项和控制来处理套接字数据发送。

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd 是套接字描述符。

  • buf 是包含要发送数据的缓冲区的指针。

  • len 是要发送的数据字节数。

  • flags 是一组标志,可以用来控制发送操作的行为,例如设置非阻塞发送等。(后续用到再介绍)

  • 返回值是发送成功的字节数,如果发送失败则返回-1。

2.8 read和recv:读数据

read函数:通用的文件读取函数,可以用于从文件描述符读取数据,也可以用于套接字。

ssize_t read(int fd, void *buf, size_t count);
  • fd 是文件描述符,可以是套接字描述符,也可以是其他文件描述符。
  • buf 是接收数据的缓冲区的指针。
  • count是要读取的数据字节数。
  • 返回值是接收成功的字节数,如果接收失败则返回-1,并使用全局变量errno设置错误代码。

recv函数:专门用于套接字通信的函数,它提供了更多的选项和控制来处理套接字数据接收。

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd是套接字描述符。
  • buf是接收数据的缓冲区的指针。
  • len是要接收的数据字节数。
  • flags是一组标志,可以用来控制接收操作的行为,例如设置非阻塞接收等。(后续用到再介绍)
  • 返回值是接收成功的字节数,如果接收失败则返回-1。

3 客户端/服务端模型实现

下面实现一个客户端/服务端模型的例子,具体功能为:客户端发一个字符串给服务端,服务端收到后打印。

3.1 代码

(1)服务端代码

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

int main() {
    // 创建套接字
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("Socket creation failed");
        exit(1);
    }

    // 绑定套接字到IP地址和端口
    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(8080);
    server_address.sin_addr.s_addr = INADDR_ANY;
    //server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
    if (bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
        perror("Bind failed");
        close(server_socket);
        exit(1);
    }

    // 设置服务器套接字为监听状态
    if (listen(server_socket, 5) == -1) {
        perror("Listen failed");
        close(server_socket);
        exit(1);
    }

    printf("Server listening on port 8080...\n");

    // 接受客户端连接
    struct sockaddr_in client_address;
    socklen_t client_len = sizeof(client_address);
    int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_len);
    if (client_socket == -1) {
        perror("Accept failed");
        close(server_socket);
        exit(1);
    }
    char buffer[1024];
    memset(buffer, 0, sizeof(buffer));
    // 从客户端接收数据
    ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer), 0);
    if (bytes_received == -1) {
        perror("Receive failed");
    } else {
        printf("Client says: %s\n", buffer);
    }
    // 发送响应给客户端
    const char *response = "Hello from server!";
    send(client_socket, response, strlen(response), 0);
    // 关闭套接字
    close(client_socket);
    close(server_socket);

    return 0;
}

(2)客户端代码

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

int main() {
    // 创建套接字
    int client_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (client_socket == -1) {
        perror("Socket creation failed");
        exit(1);
    }

    // 设置服务器地址和端口
    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(8080);
    //server_address.sin_addr.s_addr = INADDR_ANY;
	server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
    // 连接到服务器
    if (connect(client_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
        perror("Connection failed");
        close(client_socket);
        exit(1);
    }

    // 发送数据给服务器
    const char *message = "Hello from client!";
    send(client_socket, message, strlen(message), 0);

    // 接收服务器的响应
    char buffer[1024];
    memset(buffer, 0, sizeof(buffer));
    ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer), 0);
    if (bytes_received == -1) {
        perror("Receive failed");
    } else {
        printf("Server says: %s\n", buffer);
    }

    // 关闭套接字
    close(client_socket);

    return 0;
}

服务端IP地址设置为INADDR_ANY

当服务器端将IP地址设置为 INADDR_ANY 时,假设服务器上有多个网卡,那么它将绑定到所有可用的网络接口和IP地址。这意味着服务器将监听所有网络接口上的传入连接请求,而不限制于特定的IP地址。这通常用于创建一个通用的服务器,它可以接受来自任何网络接口和任何IP地址的连接请求。

对于客户端来说,通常不会将IP地址设置为 INADDR_ANY,因为客户端通常是主动发起连接请求的一方,而不是绑定到特定的IP地址。客户端通常不需要关心绑定到哪个IP地址,而是根据服务器的IP地址和端口号来连接到服务器。

3.2 运行结果

编译后,运行server程序,服务端监听8080端口:

在这里插入图片描述

运行client程序后,client与server建立连接,客户端发送的Hello from client!,服务端收到后打印出来:

在这里插入图片描述

3.3 Bind failed:Address already in use

如下图所示,在服务端程序退出后马上再运行服务端,会提示Adress already in use。
在这里插入图片描述
当一个套接字绑定到一个特定的IP地址和端口后,如果你关闭该套接字,操作系统通常会保留一段时间,称为TIME_WAIT状态,以确保任何挂起的数据包都能够到达其目的地。这样可以避免在相同的地址和端口上立即重新绑定套接字时出现问题。

解决
在创建套接字时使用setsockopt函数设置SO_REUSEADDR选项,以允许在TIME_WAIT状态下重新绑定相同的地址和端口。

int reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

在前面的服务端代码中增加上面的代码:
在这里插入图片描述

可以看到,在服务端退出后,再马上运行服务端的程序不会出现Address already in use的提示
在这里插入图片描述

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tilblackout

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值