当前位置: 首页 > article >正文

【Linux网络】——Socket网络编程

前言

  在当今数字化的时代,网络通信已经成为计算机领域不可或缺的一部分。无论是日常的网页浏览、社交媒体交互,还是大规模的企业级数据传输,都离不开高效可靠的网络通信。而在Linux操作系统中,Socket网络编程是实现各种网络应用的基础技术。通过使用Socket编程,开发人员可以在不同的计算机之间建立连接,实现数据的发送和接收,为各种网络应用程序的开发和部署提供了强大的支持。


Socket网络编程

基本概念

Socket是一种用于在网络中传输数据的抽象接口。它定义了应用程序与网络操作系统之间的通信接口,允许应用程序通过网络进行数据交换。Socket可以看作是一个通信的端点,包含了IP地址和端口号。

ip地址

  • 标识网络中设备唯一身份的数字标签。
  • 在IPv4中,IP地址由32位二进制数表示,通常以点分十进制表示法表示,如192.168.1.1。而在IPv6中,IP地址由128位二进制数表示,采用冒号十六进制表示法。

端口号

  • 端口号用于标识网络应用程序在同一台计算机上的不同连接。
  • 端口号由16位无符号整数表示,范围从0到65535。端口0到1023是系统保留端口,通常用于特定的服务,如HTTP(80)、FTP(21)等。自定义应用程序通常使用1024以上的端口号。

IP地址和端口号互相配合,IP地址负责找到唯一的一台网络设备,端口号负责找到这台设备上的唯一连接,就组成了唯一的一个地址,让数据有了起点和终点

Socket网络编程接口

理解了Socket编程所需的基本概念,接下来,就需要看看其接口

创建套接字——socket

在开始网络通信之前,需要创建一个套接字。

在Linux中,通过调用socket()系统调用来创建套接字。

函数造型:

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

参数说明:

  • domain:地址族,常用的有AF_INET(IPv4)、AF_INET6(IPv6)。
  • type:套接字类型,如SOCK_STREAM(TCP协议)SOCK_DGRAM(UDP协议)
  • protocol:协议类型,通常设为0,表示默认协议。

返回值说明:

  • 成功时返回一个非负整数,表示Socket描述符。
  • 失败时返回 -1,并设置 errno

绑定套接字——bind

将Socket绑定到一个特定的IP地址和端口。

绑定地址后,该套接字相当于在网络中有了唯一性,这些必不可少的一环

函数造型

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

参数说明

  • sockfd: Socket描述符。
  • addr: 指向包含IP地址和端口信息的 sockaddr 结构体。
  • addrlenaddr 结构体的大小。

返回值说明

  • 成功时返回 0
  • 失败时返回 -1,并设置 errno

sockaddr结构体

单独将sockaddr结构体拎出来讲一下

在Linux的Socket编程中,sockaddr 结构体是一个通用的地址结构体,用于表示网络地址信息。

它的基本形式如下:

struct sockaddr {
    sa_family_t sa_family;  // 地址族 (Address family)
    char        sa_data[14]; // 协议地址 (Protocol address)
};

成员说明:

  • sa_family: 地址族,表示地址的类型。常用的值包括:
    • AF_INET: IPv4 地址族
    • AF_INET6: IPv6 地址族
    • AF_UNIX: Unix 域套接字(用于本地进程间通信)
    • 其他地址族(如 AF_PACKET 用于原始套接字等)
  • sa_data: 这是一个14字节的数组,用于存储具体的协议地址信息。对于不同的地址族,sa_data 中存储的内容不同。例如,对于 AF_INETsa_data 包含IP地址和端口号。

不过,sockaddr 是一个通用的结构体,实际使用时通常会使用更具体的结构体,如 sockaddr_in(用于IPv4)或 sockaddr_in6(用于IPv6)。

sockaddr_in 结构体(用于IPv4)

在实际编程中,sockaddr_in 结构体更常用于表示IPv4地址信息。它是对 sockaddr 的扩展,提供了更明确的字段来表示IPv4地址和端口。

基本样式

struct sockaddr_in {
    sa_family_t    sin_family; // 地址族,必须是 AF_INET
    in_port_t      sin_port;   // 端口号,使用网络字节序(大端)
    struct in_addr sin_addr;   // IP地址,使用网络字节序
    char           sin_zero[8]; // 填充字段,与 `sockaddr` 的大小对齐
};

成员说明:

  • sin_family: 地址族,必须设置为 AF_INET
  • sin_port: 端口号,使用网络字节序(大端)。可以使用 htons() 函数将主机字节序转换为网络字节序。
  • sin_addr: IP地址,使用 in_addr 结构体表示。in_addr 结构体内部只有一个成员s_addr用来存储IP地址
  • sin_zero: 这是一个8字节的填充字段,用于与 sockaddr 结构体的大小对齐。通常设置为0。 

sockaddr_in6 结构体(用于IPv6)

对于IPv6地址,可以使用 sockaddr_in6 结构体

基本样式:

struct sockaddr_in6 {
    sa_family_t     sin6_family;   // 地址族,必须是 AF_INET6
    in_port_t       sin6_port;     // 端口号,使用网络字节序
    uint32_t        sin6_flowinfo; // IPv6流信息
    struct in6_addr sin6_addr;     // IPv6地址
    uint32_t        sin6_scope_id; // 作用域ID(用于链路本地地址)
};

关于sockaddr_in6我们不过多介绍,因为实际socket中基本不使用,很难见到

监听套接字——listen

将Socket设置为监听状态,等待客户端的连接请求。

基本样式

int listen(int sockfd, int backlog);

参数说明

  • sockfd: Socket描述符。
  • backlog: 指定等待连接队列的最大长度。

返回值说明

  • 成功时返回 0
  • 失败时返回 -1,并设置 errno

之前进行三次握手的时候说过,会有一个连接队列,用于暂时保存等待的连接

这里的listen就可以设置改连接队列的大小,指定等待连接的数量

接受连接——accept

接受客户端的连接请求,并返回一个新的Socket描述符用于与客户端通信。

函数造型:

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

 参数说明:

  • sockfd: 监听Socket描述符。
  • addr: 指向存储客户端地址信息的 sockaddr 结构体。
  • addrlen: 指向 addr 结构体大小的指针。

返回值说明:

  • 成功时返回一个新的Socket描述符,用于与客户端通信。
  • 失败时返回 -1,并设置 errno

请求连接——coonect

客户端使用该函数连接到服务器。

函数造型

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

参数说明:

  • sockfd: Socket描述符。
  • addr: 指向包含服务器IP地址和端口信息的 sockaddr 结构体。
  • addrlenaddr 结构体的大小。

返回值说明:

  • 成功时返回 0
  • 失败时返回 -1,并设置 errno

发送与接受——send 和 recv

send() 用于发送数据,recv() 用于接收数据。

两者均需要使用被绑定过,且已被连接的套接字来捕获数据

基本样式

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明

  • sockfd: Socket描述符。
  • buf: 指向发送或接收数据的缓冲区。
  • len: 数据的长度。
  • flags: 通常为0,表示阻塞模式。

注意:

使用socket套接字进行网络通信以前,必须对套接字进行绑定和连接建立!否则无法发送或者接收数据

socket网络通信

服务端网络编程

了解完接口,接下来尝试使用socket套接字完成服务器的搭建

服务器搭建一般流程:

创建套接字
绑定套接字
监听连接
接受连接
发送与接受
关闭套接字

1.创建套接字

 //创建套接字
    int socket_fd = socket(AF_INET,SOCK_STREAM,0);

2.绑定套接字 

    //先创建一个struct sockaddr_in结构体对象并填充
    struct sockaddr_in serv_addr;
    //将内部清零
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY表示监听所有的接口
    serv_addr.sin_port = htons(8080);       // 填充端口号

    bind(socket_fd,(sockaddr*)(&serv_addr),sizeof(serv_addr)

注意:

  • 我们这边并没有将服务端的ip地址设置成唯一的,而是使用INADDR_ANY,是表示监听所有ip地址的连接,因为一台网络设备不一定只有一个ip地址

3.监听套接字 

int length = 1;
    if(listen(socket_fd,length)!=0){
        perror("listen failed");
        close(socket_fd);
        exit(EXIT_FAILURE);
    }

注意:

  • 这个等待队列是可以进行根据自己的需求进行设置

4.接收连接

    //连接套接字
    struct sockaddr_in cilent_addr;
    memset(&cilent_addr,0,sizeof(cilent_addr));
    socklen_t cilent_len = sizeof(cilent_addr);
    int connect_fd = accept(socket_fd,(sockaddr*)(&cilent_addr),&cilent_len);

5.发送与接收

while(flag){
        char recv_buff[1024];
        //这里用于发送数据和接受数据
        recv(connect_fd,recv_buff,sizeof(recv_buff),0);
        std::cout<<"cilent say:"<<recv_buff<<std::endl;

        char send_buff[1024] = "server have got it!";
        send(connect_fd,send_buff,sizeof(send_buff),0);
    }

6.关闭连接 

close(socket_fd);
close(connect_fd);

 客户端网络编程

客户端搭建一般流程:

创建套接字
设置服务器信息
请求连接
数据发送与接收
关闭连接

1.创建套接字

 //创建套接字
 int socket_fd = socket(AF_INET,SOCK_STREAM,0);

 2.设置服务器信息

//设置服务器地址结构
    struct sockaddr_in server_addr;
    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    inet_pton(AF_INET,server_ip.c_str(),&server_addr.sin_addr);

3.请求连接 

 if(connect(socket_fd,(sockaddr*)(&server_addr),sizeof(server_addr))!= 0){
        perror("connect failed");
        close(socket_fd);
        exit(EXIT_FAILURE);
 }

 注意:

  • 客户端并不需要自己设置自己的ip地址和端口,一旦连接建立成功,发送数据,这些都会自己绑定

客户端的ip地址是唯一的,但端口号不唯一,而是操作系统随机分配的

为什么端口号要随机分配?

如果一个主机起了多个客户端,这些客户端都会进行抢占端口

如果不进行随机绑定,而是特定端口的话

就会出现两个客户端同时使用一个端口的情况,导致其中一个客户端无法通信

例如

  • 夸克使用8080端口,网易云也使用8080端口
  • 夸克客户端先启动,就会先占用这个端口,网易云就不能再使用这个端口,无法进行通信

因为一个端口只能一个进程,另一个进程就无法通信

4.数据的发生与接收

 //接受和发送数据
    while(flag){
        char recv_buff[1024];
        char send_buff[1024] = "hello ,i am cilent";
        send(socket_fd,send_buff,sizeof(send_buff),0);
        //这里用于发送数据和接受数据
        recv(socket_fd,recv_buff,sizeof(recv_buff),0);
        std::cout<<"server say:"<<recv_buff<<std::endl;
    }

5.关闭连接

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

通信示例

启动服务端

./server

启动客户端

./cilent

运行结果:

服务端

客户端

其实网络通信也是进程通信的一种

不过由原来的主机之间不同进程通信,变成了不同主机间的进程通信。 


结语

  Socket编程的学习是一个不断深入的过程,每一次的实践都是对网络通信理解的深化。希望本文能够为你在Socket编程的学习道路上提供一些帮助,并激发你对网络编程的兴趣和探索欲望。

  在未来的学习和实践中,愿你能够不断挑战自我,掌握更多的网络编程技能,创造出更加出色的网络应用程序!无论你是继续深入研究网络通信的底层原理,还是专注于开发高性能的网络服务,Socket编程都将是你不可或缺的工具和基石。编程之路虽长,但每一步都充满了成长的机会和无限的可能。加油!


http://www.kler.cn/a/610303.html

相关文章:

  • 6. 理解中间件与认证中间件
  • 失踪人口回归之Java开发工程师面试记录第二篇
  • AI小白的第七天:必要的数学知识(概率)
  • 前端面试:如何去衡量用户操作过程中否卡顿?
  • LLM实践(二)——基于llama-factory的模型微调
  • 蓝桥杯高频考点——二分(含C++源码)
  • Go 1.24 新特性解析:泛型类型别名、弱指针与终结器改进
  • 论文阅读笔记——Diffuser,Diffusion Policy
  • java使用小知识合集(持续更新中)
  • 栈-常见考察面试算法题
  • 生活电子常识——cmd不能使用anaconda的python环境,导致输入python打开应用商店
  • Driver具体负责什么工作
  • 大疆上云api直播功能如何实现
  • odata 搜索帮助
  • LangChain开发(六)多模态输入与自定义输出
  • leetcode238.除自身意外数组的乘积
  • 解决GLIBC不兼容问题
  • 查看linux系统文件描述符限制
  • abp vnext框架重写volo.abp.openiddict的tokenController登录验证
  • 【MySQL】用户账户、角色、口令、PAM