【计算机网络】Socket网络编程

目录

一、主机字节序列和网络字节序列

二、套接字地址结构

[1、IPv4 地址结构 (sockaddr_in)](#1、IPv4 地址结构 (sockaddr_in))

[2、IPv6 地址结构 (sockaddr_in6)](#2、IPv6 地址结构 (sockaddr_in6))

3、通用套接字地址结构 (sockaddr)

4、Unix域套接字地址结构 (sockaddr_un)

[5、专用 socket 地址结构](#5、专用 socket 地址结构)

6、套接字地址结构的转换

字符串转二进制地址

二进制地址转字符串

7、端口号的转换

三、网络编程接口

socket

bind

listen

accept

[TCP 数据读写:](#TCP 数据读写:)

[UDP 数据读写:](#UDP 数据读写:)

close

connect

四、TCP编程流程

代码示例:

服务端

客户端


一、主机字节序列和网络字节序列

主机字节序列(Host Byte Order)指的是计算机在内存中存储多字节数据时的顺序。分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。

  • 大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。
  • 小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。

在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。

Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换:

c 复制代码
#include <arpa/inet.h>
#include <netinet/in.h>

uint32_t htonl(uint32_t hostlong); // 主机到网络(长整型)长整型的主机字节序转网络字节序
uint16_t htons(uint16_t hostshort); // 主机到网络(短整型)短整形的主机字节序转网络字节序
uint32_t ntohl(uint32_t netlong);   // 网络到主机(长整型)长整型的网络字节序转主机字节序
uint16_t ntohs(uint16_t netshort);  // 网络到主机(短整型)短整型的网络字节序转主机字节序
 

不同架构的处理器可能使用不同的字节序。x86架构通常是小端序,而网络协议(如TCP/IP)要求使用大端序。因此,跨平台通信时必须进行字节序转换。

二、套接字地址结构

套接字地址结构用于在网络编程中存储通信所需的地址信息。常见的套接字地址结构包括IPv4、IPv6和Unix域套接字地址结构。

1、IPv4 地址结构 (sockaddr_in)

IPv4套接字地址结构定义在<netinet/in.h>中,用于存储IPv4地址和端口号。

c 复制代码
struct sockaddr_in {
    sa_family_t    sin_family; // 地址族,如AF_INET
    in_port_t      sin_port;   // 16位端口号,网络字节序
    struct in_addr sin_addr;   // 32位IPv4地址,网络字节序
    char           sin_zero[8]; // 填充字段,通常置零
};

struct in_addr {
    uint32_t s_addr; // IPv4地址,网络字节序
};

2、IPv6 地址结构 (sockaddr_in6)

IPv6套接字地址结构用于存储IPv6地址和端口号。

c 复制代码
struct sockaddr_in6 {
    sa_family_t     sin6_family;   // 地址族,如AF_INET6
    in_port_t       sin6_port;     // 16位端口号,网络字节序
    uint32_t        sin6_flowinfo; // 流信息
    struct in6_addr sin6_addr;     // 128位IPv6地址,网络字节序
    uint32_t        sin6_scope_id; // 范围ID
};

struct in6_addr {
    unsigned char s6_addr[16]; // IPv6地址,网络字节序
};

3、通用套接字地址结构 (sockaddr)

socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,通用套接字地址结构用于在函数参数中传递不同类型的地址结构。其定义如下:

c 复制代码
struct sockaddr {
    sa_family_t sa_family; // 地址族
    char        sa_data[14]; // 协议地址
};

sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下图所示:

4、Unix域套接字地址结构 (sockaddr_un)

Unix域套接字用于本地进程间通信,其地址结构定义在<sys/un.h>中。

c 复制代码
struct sockaddr_un {
    sa_family_t sun_family; // 地址族,AF_UNIX
    char        sun_path[108]; // 文件路径名
};

5、专用 socket 地址结构

TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分别用于 IPV4 和 IPV6:

cpp 复制代码
/*
sin_family: 地址族 AF_INET
sin_port: 端口号,需要用网络字节序表示
sin_addr: IPV4 地址结构:s_addr 以网络字节序表示 IPV4 地址
*/

struct in_addr{
    u_int32_t s_addr;
};
struct sockaddr_in{
    sa_family_t sin_family;
    u_int16_t sin_port;
    struct in_addr sin_addr;
};
struct in6_addr{
    unsigned char sa_addr[16]; // IPV6 地址,要用网络字节序表示
};
struct sockaddr_in6{
    sa_family_t sin6_family; // 地址族:AF_INET6
    u_inet16_t sin6_port; // 端口号:用网络字节序表示
    u_int32_t sin6_flowinfo; // 流信息,应设置为 0
    struct in6_addr sin6_addr; // IPV6 地址结构体
    u_int32_t sin6_scope_id; // scope ID,尚处于试验阶段
};

6、套接字地址结构的转换

在网络编程中,通常需要将人类可读的IP地址和端口号转换为网络字节序的二进制形式。

字符串转二进制地址

使用inet_pton函数将点分十进制字符串转换为二进制地址。

c 复制代码
#include <arpa/inet.h>

const char *ip_str = "192.168.1.1";
struct in_addr addr;
inet_pton(AF_INET, ip_str, &addr);
二进制地址转字符串

使用inet_ntop函数将二进制地址转换为点分十进制字符串。

c 复制代码
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, ip_str, INET_ADDRSTRLEN);

7、端口号的转换

端口号需要转换为网络字节序(大端序)后使用。

c 复制代码
#include <arpa/inet.h>

uint16_t port = 8080;
uint16_t net_port = htons(port); // 主机字节序转网络字节序
uint16_t host_port = ntohs(net_port); // 网络字节序转主机字节序

三、网络编程接口

网络编程接口(API,Application Programming Interface)是不同软件系统间进行通信和数据交换的标准化方式。通过网络API,开发者可以调用远程服务、获取数据或执行特定操作,而无需了解底层实现细节。

socket

socket()创建套接字,成功返回套接字的文件描述符,失败返回-1

domain: 设置套接字的协议簇, AF_UNIX AF_INET AF_INET6 ;type: 设置套接字的服务类型 SOCK_STREAM SOCK_DGRAM ;protocol: 一般设置为 0,表示使用默认协议

cpp 复制代码
int socket(int domain, int type, int protocol);

bind

bind()将 sockfd 与一个 socket 地址绑定,成功返回 0,失败返回-1

sockfd 是网络套接字描述符 ;addr 是地址结构 ;addrlen 是 socket 地址的长度

cpp 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen

listen()创建一个监听队列以存储待处理的客户连接,成功返回 0,失败返回-1

sockfd 是被监听的 socket 套接字 ;backlog 表示处于完全连接状态的 socket 的上限

cpp 复制代码
int listen(int sockfd, int backlog);

accept

accept()从 listen 监听队列中接收一个连接,成功返回一个新的连接 socket,该 socket 唯一地标识了被接收的这个连接,失败返回-1

sockfd 是执行过 listen 系统调用的监听 socket ;addr 参数用来获取被接受连接的远端 socket 地址 ;addrlen 指定该 socket 地址的长度

cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

TCP 数据读写:

recv()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小

send()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长度

flags 参数为数据收发提供了额外的控制

cpp 复制代码
ssize_t recv(int sockfd, void *buff, size_t len, int flags);

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

UDP 数据读写:

recvfrom()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小

src_addr 记录发送端的 socket 地址 ;addrlen 指定该地址的长度 ;sendto()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长度 ;dest_addr 指定接收数据端的 socket 地址 ;addrlen 指定该地址的长度

cpp 复制代码
ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags,

struct sockaddr* src_addr, socklen_t *addrlen);

ssize_t sendto(int sockfd, void *buff, size_t len, int flags,

struct sockaddr* dest_addr, socklen_t addrlen);

close

close()关闭一个连接,实际上就是关闭该连接对应的 socket

cpp 复制代码
int close(int sockfd);

connect

connect()客户端需要通过此系统调用来主动与服务器建立连接,成功返回 0,失败返回-1

sockfd 参数是由 socket()返回的一个 socket。 serv_addr 是服务器监听的 socket 地址 ;addrlen 则指定这个地址的长度

cpp 复制代码
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

四、TCP编程流程

TCP 提供的是面向连接的、可靠的、字节流服务。TCP 的服务器端和客户端编程流程如下:

  • socket()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。这也是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用的服务类型,使用 TCP 协议选择流式服务(SOCK_STREAM)。
  • bind()方法是用来指定套接字使用的 IP 地址和端口。IP 地址就是自己主机的地址,如果主机没有接入网络,测试程序时可以使用回环地址"127.0.0.1"。端口是一个 16 位的整形值,一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在Linux 上,1024 以内的端口号,只有 root 用户可以使用。
  • listen()方法是用来创建监听队列。监听队列有两种,一个是存放未完成三次握手的连接,一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。
  • accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
  • connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方法执行后,会进行三次握手, 建立连接。
  • send()方法用来向 TCP 连接的对端发送数据。send()执行成功,只能说明将数据成功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入到发送缓冲区中的数据长度。
  • recv()方法用来接收 TCP 连接的对端发送来的数据。recv()从本端的接收缓冲区中读取数据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果 recv()返回值为 0, 说明对方已经关闭了 TCP 连接。
  • close()方法用来关闭 TCP 连接。此时,会进行四次挥手。

代码示例:

服务端

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>

int main(){
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    /*创建套接字,tcp协议
    返回值为文件描述符,0标准输入,1标准输出,2标准错误输出,3=sockfd;
    参数为地址图、服务类型(tcp为流式服、udp为数据报表)、恒为0
    */
    if( sockfd == -1 ){
        exit(1);
    }
    struct sockaddr_in saddr;//定义ipv4专用的地址结构
    memset(&saddr,0,sizeof(saddr));//清空,参数为套接字地址,0,套接字大小
    saddr.sin_family = AF_INET;//填充地址图
    saddr.sin_port = htons(6000);//填充端口,大端,主机字节序列转为网络字节序列,
    //1024以内知名端口,root;1024-4096保留端口一般不用;4096以上为临时端口,随便用
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip地址,转为无符号整型
    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    /*绑定,设置ip和端口
    参数为:指定地址的的套接字,地址,地址结构大小
    */
    if( res == -1){
        printf("bind err\n");
        exit(1);
    }
    res = listen(sockfd,5);
    /*创建监听队列,存放客户端发送来的链接请求
    参数为:套接字对应的描述符,整数值:监听大小
    创建两个监听队列,一个是存放未完成三次握手的连接,一种是存放已完成三次握手的连接。
    listen()第二个参数在linux就是指定已完成三次握手队列的长度,在unix上是表示已完成+未完成。
    accept()处理已完成三次握手的连接
    */
    if( res == -1){
        exit(1);
    }
    while( 1 ){
        struct sockaddr_in caddr;//记录客户端地址
        socklen_t len = sizeof(caddr);//计算大小
        int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
        /*接受套接字,阻塞
        参数为(接收套接字,存放客户端地址的套接字,指针)
        */
        printf("c=%d,ip:%s,port=%d\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
        //ip转化为点分十进制的字符串,端口将网络字节序列转化为主机字节序列
        while( 1 ){
            char buff[128] = {0};
            int num = recv(c,buff,127,0);//read
             if( num <= 0 ){// n==0 对方关闭,n==-1,失败
                break;
            }
            printf("buff=%s\n",buff);//接收之后,打印
            send(c,"ok",2,0);//write,描述符,带发送的数据,大小,标志位
        }
        printf("client close\n");
        close(c);
    }
    close(sockfd);
    exit(0);
}

客户端

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
int main(){
    int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建套接字
    if( sockfd == -1){
        exit(1);
    }
    struct sockaddr_in saddr;//ipv4地址
    bzero(&saddr, sizeof(saddr)); //memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);//因为服务器用的是6000
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip
    int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if( res == -1){
        printf("connect err\n");
        exit(1);
    }
    while(1){
        char buff[128] = {0};
        printf("input:\n");
        fgets(buff,128,stdin);//从键盘获取数据
        if(strncmp(buff,"end",3)==0){
            break;
        }
        send(sockfd,buff,strlen(buff)-1,0);
        memset(buff,0,128);
        recv(sockfd,buff,127,0);
        printf("buff=%s\n",buff);
    }
    close(sockfd);
    exit(0);
}

执行结果:

相关推荐
leoFY1232 小时前
STM32H750配置LAN PHY芯片LAN8742
网络·stm32·嵌入式硬件
阿部多瑞 ABU2 小时前
AI红队攻防演化史(2023-2026):从虚拟角色到RLHF劫持——所有攻击方法全景总结与最新趋势分析
网络·人工智能·安全
古月方枘Fry2 小时前
MGRE实验
运维·服务器
博客-小覃3 小时前
Zabbix之华为交换机的日志记录信息操作详细教程
服务器·网络·华为·zabbix
stolentime3 小时前
FreeDomain 本地开发环境快速搭建指南
运维·服务器·网络
向量引擎3 小时前
从零起步,如何打造专属向量引擎 API 中转工作流?
java·服务器·前端
z200509304 小时前
【Linux学习】Linux中的进程程序替换
linux·服务器·学习
ytdbc4 小时前
OSPF综合实验
网络
kaisun645 小时前
Docker 构建网络问题排查
网络·docker·eureka
lihao lihao5 小时前
软硬链接
linux·运维·服务器