socket编程 3个月前

编程语言
653
socket编程

Socket套接字由远景研究规划局(Advanced Research Projects Agency, ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发。 其目的是将TCP/IP协议相关软件移植到UNIX类系统中。设计者开发了一个接口,以便应用程序能简单地调用该接口通信。 这个接口不断完善,最终形成了Socket套接字。Linux系统采用了Socket套接字,因此,Socket接口就被广泛使用,到现在已经成为事实上的标准。 与套接字相关的函数被包含在头文件sys/socket.h中。

套接字对我们来说就是一套网络通信的接口,使用这套接口就可以完成网络通信。 网络通信的主体主要分为两部分:客户端服务器端。 在客户端和服务器通信的时候需要频繁提到三个概念:IP端口通信数据

1. 字节序

在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编/译码从而导致通信失败。

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序 也就是说对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。

目前在各种体系的计算机中通常采用的字节存储机制主要有两种:Big-EndianLittle-Endian

下面先从字节序说起。

大小端的这个名词最早出现在《格列佛游记》中,里边记载了两个征战的强国,你不会想到的是,他们打仗竟然和剥鸡蛋的顺序有关。很多人认为,剥鸡蛋时应该打破鸡蛋较大的一端,这群人被称作“大端(Big endian)派”。可是那时皇帝儿子小时候吃鸡蛋的时候碰巧将一个手指弄破了。所以,当时的皇帝就下令剥鸡蛋必须打破鸡蛋较小的一端,违令者重罚,由此产生了“小端(Little endian)派”。 老百姓们对这项命令极其反感,由此引发了6次叛乱,其中一个皇帝送了命,另一个丢了王位。据估计,先后几次有11000人情愿受死也不肯去打破鸡蛋较小的一端!

  • Little-Endian -> 主机字节序 (小端)
    • 数据的低位字节存储到内存的低地址位, 数据的高位字节存储到内存的高地址位
    • 我们使用的PC机,数据的存储默认使用的是小端
  • Big-Endian -> 网络字节序 (大端)
    • 数据的低位字节存储到内存的高地址位, 数据的高位字节存储到内存的低地址位
    • 套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据,IP地址,端口
  • 字节序举例
// 有一个16进制的数, 有32位 (int): 0xab5c01ff
// 字节序, 最小的单位: char 字节, int 有4个字节, 需要将其拆分为4份
// 一个字节 unsigned char, 最大值是 255(十进制) ==> ff(16进制) 
                 内存低地址位                内存的高地址位
--------------------------------------------------------------------------->
小端:         0xff        0x01        0x5c        0xab
大端:         0xab        0x5c        0x01        0xff

image

  • 函数

BSD Socket提供了封装好的转换接口,方便我们使用。 包括从主机字节序到网络字节序的转换函数:htons、htonl; 从网络字节序到主机字节序的转换函数:ntohs、ntohl。

#include <arpa/inet.h>
// u:unsigned
// 16: 16位, 32:32位
// h: host, 主机字节序
// n: net, 网络字节序
// s: short
// l: int

// 这套api主要用于 网络通信过程中 IP 和 端口 的 转换
// 将一个短整形从主机字节序 -> 网络字节序
uint16_t htons(uint16_t hostshort);    
// 将一个整形从主机字节序 -> 网络字节序
uint32_t htonl(uint32_t hostlong);    

// 将一个短整形从网络字节序 -> 主机字节序
uint16_t ntohs(uint16_t netshort)
// 将一个整形从网络字节序 -> 主机字节序
uint32_t ntohl(uint32_t netlong);

2. IP地址转换

虽然IP地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述 下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换:

// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst);
  • 参数:
    • af: 地址族(IP地址的家族包括ipv4和ipv6)协议
      • AF_INET: ipv4格式的ip地址
      • AF_INET6: ipv6格式的ip地址
    • src: 传入参数, 对应要转换的点分十进制的ip地址: 192.168.1.100
    • dst: 传出参数, 函数调用完成, 转换得到的大端整形IP被写入到这块内存中
  • 返回值:成功返回1,失败返回0或者-1
#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址        
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • 参数
    • af: 地址族协议
      • AF_INET: ipv4格式的ip地址
      • AF_INET6: ipv6格式的ip地址
    • src: 传入参数, 这个指针指向的内存中存储了大端的整形IP地址
    • dst: 传出参数, 存储转换得到的小端的点分十进制的IP地址
    • size: 修饰dst参数的, 标记dst指向的内存中最多可以存储多少个字节
  • 返回值:
    • 成功: 指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串
    • 失败: NULL

还有一组函数也能进程IP地址大小端的转换,但是只能处理ipv4的ip地址:

// 点分十进制IP -> 大端整形
in_addr_t inet_addr (const char *cp);

// 大端整形 -> 点分十进制IP
char* inet_ntoa(struct in_addr in);

3. sockaddr 数据结构

image

// 在写数据的时候不好用
struct sockaddr {
    sa_family_t sa_family;       // 地址族协议, ipv4
    char        sa_data[14];     // 端口(2字节) + IP地址(4字节) + 填充(8字节)
}

typedef unsigned short  uint16_t;
typedef unsigned int    uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

struct in_addr
{
    in_addr_t s_addr;
};  

// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in
{
    sa_family_t sin_family;        /* 地址族协议: AF_INET */
    in_port_t sin_port;         /* 端口, 2字节-> 大端  */
    struct in_addr sin_addr;    /* IP地址, 4字节 -> 大端  */
    /* 填充 8字节 */
    unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
               sizeof (in_port_t) - sizeof (struct in_addr)];
};

4. 套接字函数

使用套接字通信函数需要包含头文件<arpa/inet.h>,包含了这个头文件<sys/socket.h>就不用在包含了。

// 创建一个套接字
int socket(int domain, int type, int protocol);
  • 参数:
    • domain: 使用的地址族协议
      • AF_INET: 使用IPv4格式的ip地址
      • AF_INET6: 使用IPv4格式的ip地址
    • type:
      • SOCK_STREAM: 使用流式的传输协议
      • SOCK_DGRAM: 使用报式(报文)的传输协议
    • protocol: 一般写0即可, 使用默认的协议
      • SOCK_STREAM: 流式传输默认使用的是tcp
      • SOCK_DGRAM: 报式传输默认使用的udp
  • 返回值:
    • 成功: 可用于套接字通信的文件描述符
    • 失败: -1

函数的返回值是一个文件描述符,通过这个文件描述符可以操作内核中的某一块内存,网络通信是基于这个文件描述符来完成的。

// 将文件描述符和本地的IP与端口进行绑定   
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数:
    • sockfd: 监听的文件描述符, 通过socket()调用得到的返回值
    • addr: 传入参数, 要绑定的IP和端口信息需要初始化到这个结构体中,IP和端口要转换为网络字节序
    • addrlen: 参数addr指向的内存大小, sizeof(struct sockaddr)
  • 返回值:成功返回0,失败返回-1
// 给监听的套接字设置监听
int listen(int sockfd, int backlog);
  • 参数:
    • sockfd: 文件描述符, 可以通过调用socket()得到,在监听之前必须要绑定 bind()
    • backlog: 同时能处理的最大连接要求,最大值为128
  • 返回值:函数调用成功返回0,调用失败返回 -1
// 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)        
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 参数:
    • sockfd: 监听的文件描述符
    • addr: 传出参数, 里边存储了建立连接的客户端的地址信息
    • addrlen: 传入传出参数,用于存储addr指向的内存大小
  • 返回值:函数调用成功,得到一个文件描述符, 用于和建立连接的这个客户端通信 调用失败返回 -1这个函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞;当检测到有新的客户端连接请求时,阻塞解除,新连接就建立了,得到的返回值也是一个文件描述符,基于这个文件描述符就可以和客户端通信了。
// 接收数据
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
  • 参数:
    • sockfd: 用于通信的文件描述符, accept() 函数的返回值
    • buf: 指向一块有效内存, 用于存储接收是数据
    • size: 参数buf指向的内存的容量
    • flags: 特殊的属性, 一般不使用, 指定为 0
  • 返回值:
    • 大于0:实际接收的字节数
    • 等于0:对方断开了连接
    • -1:接收数据失败了

如果连接没有断开,接收端接收不到数据,接收数据的函数会阻塞等待数据到达,数据到达后函数解除阻塞,开始接收数据, 当发送端断开连接,接收端无法接收到任何数据,但是这时候就不会阻塞了,函数直接返回0。

// 发送数据的函数
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
  • 参数:
    • fd: 通信的文件描述符, accept() 函数的返回值
    • buf: 传入参数, 要发送的字符串
    • len: 要发送的字符串的长度
    • flags: 特殊的属性, 一般不使用, 指定为 0
  • 返回值:
    • 大于0:实际发送的字节数,和参数len是相等的
    • -1:发送数据失败了
      
// 成功连接服务器之后, 客户端会自动随机绑定一个端口
// 服务器端调用accept()的函数, 第二个参数存储的就是客户端的IP和端口信息
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数:
    • sockfd: 通信的文件描述符, 通过调用socket()函数就得到了
    • addr: 存储了要连接的服务器端的地址信息: iP 和 端口,这个IP和端口也需要转换为大端然后再赋值
    • addrlen: addr指针指向的内存的大小 sizeof(struct sockaddr)
  • 返回值:连接成功返回0,连接失败返回-1
image
EchoEcho官方
无论前方如何,请不要后悔与我相遇。
1377
发布数
439
关注者
2222838
累计阅读

热门教程文档

Spring Boot
24小节
Flutter
105小节
Linux
51小节
Docker
62小节
Gin
17小节