[Linux][网络][网络编程套接字][二][Socket编程接口][地址转换函数][TCP协议通讯流程]详细讲解


1.socket编程接口

1.创建 socket 文件描述符(TCP/UDP,客户端 + 服务器)

  • 原型:int socket(int domain, int type, int protocol);
  • **功能:**打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符(本质就是打开网络文件)
  • 参数:
  • domain:指明协议族,即想要使用什么协议(IPV4、IPV6…),它是下列表格中的某个常值。该参数也往往被称为协议域
AF_INETIPV4协议
AF_INET6IPV6协议
AF_LOCALUnix域协议
AF_ROUTE路由套接字
AF_KEY密钥套接字
  • type:指明套接字的类型,它是下列表格中的某个值
SOCK_STREAM字节流套接字
SOCK_DGRAM数据报套接字
SOCK_SEQPACKET有序分组套接字
SOCK_RAW原始套接字
  • protocol:创建套接字的协议类别,可以指明TCP/UDP,但该字段一般设置为0即可(0会根据前两个参数自动推导需要使用哪种协议)
IPPROTO_TCPTCP传输协议
IPPROTO_UDPUDP传输协议
IPPROTO_SCTPSCTP传输协议
  • **返回值:**成功返回一个文件描述符,失败返回-1,同时错误码被设置

2.绑定端口号(TCP/UDP,服务器)

  • 原型:int bind(int socket, const struct sockaddr *address, socklen_t address_len);
  • **功能:**给一个协议地址赋予一个套接字,使socket这个用于网络通讯的文件描述符监听address所描述的地址和端口号
    • 即:将用户设置的ip和port在内核中和当前的进程强关联
  • 参数:
    • socket**:**绑定的文件的文件描述符,也就是创建socket时获取到的文件描述符
    • address**:**指向一个特定于协议的地址结构的指针
    • address_len**:**该协议的地址结构的长度
  • **返回值:**成功返回0,失败返回-1,同时错误码被设置

3.开始监听socket(TCP,服务器)

  • 原型:int listen(int socket, int backlog);

  • **功能:**使socket处于监听状态,表明服务器对外宣告它愿意接受连接请求,它做两件事

    • 简单来说,服务器调用listen函数,就是告诉客户端你可以连接我了
    • 一旦服务器调用了listen,所用的套接字就能接受连接请求。使用accept函数获得的连接请求并建立连接
  • 参数:

    • **socket:**需要设置为监听状态的套接字对应的描述符
    • **backlog:**全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度
  • **返回值:**成功返回0,失败返回-1

  • 说明:本函数通常应该在调用socket()和bind()之后,并在调用accept()之前

4.接收请求(TCP,服务器)

  • 原型:int accept(int socket, struct sockaddr *address, socklen_t *address_len);
  • 功能:
    • 用于从已完成连接队列队头返回下一个已完成连接,如果已完成连接队列为空,那么进程将被投入睡眠**(即阻塞等待直到有客户端连接上来)**
    • 获取一个已经连接建立完成的socket套接字描述符(完成了三次握手),作为与指定客户端通信的句柄
  • 参数:
    • **socket:**特定的监听套接字,表示从该监听套接字中获取连接
    • **address:**输出型参数,用于存储对端网络相关的属性信息
    • **addrlen:**输入输出型参数,调用时传入期望读取的address结构体的长度,返回值代表实际读到的address结构体的长度
  • 说明:
    • 如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接
    • 称它的第一个参数为监听套接字(listening socket)
      • 一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命期内一直存在
  • 称它的返回值为已连接套接字(connected socket)
    • 内核为每个由服务器进程接受的客户连接创建一个已连接套接字
    • 当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭
  • 总结:
    • 返回的文件描述符是新的套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字(socket)具有相同的套接字类型和地址族
    • 传给accept的原始套接字没有关联到这个连接,而是继续保持监听状态并接受其他连接请求

5.建立连接(TCP,客户端)

  • **功能:**TCP客户用connect()来建立与TCP服务器的连接
  • 原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数:
    • sockfd**:**特定的套接字,表示通过该套接字发起连接请求
    • addr**:**指向一个特定于协议的地址结构的指针
    • addrlen**:**该协议的地址结构的长度
  • **返回值:**成功返回0,失败返回-1
  • **注意:**在connect中指定的地址是我们想要与之通信服务器地址。如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址

6.setsockopt(进阶)(简介)

  • **功能:**设置套接字描述符的属性
  • 原型:int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  • 参数:
    • **sockfd:**要设置的套接字描述符
    • level:选项定义的层次,或为特定协议的代码(如IPv4,IPv6,TCP,SCTP),或为通用套接字代码(SOL_SOCKET)
    • **optname:**选项名,level对应的选项,一个level对应多个选项,不同选项对应不同功能
    • **optval:**指向某个变量的指针,该变量是要设置新值的缓冲区,可以是一个结构体,也可以是普通变量
      • **非0:**打开功能
      • 0**:**关闭功能
    • optlen**:**optval的长度
  • **返回值:**成功返回0,失败返回-1
  • 说明:选项字段很多,需要的去查文档即可
  • 实用举例:
// 将IP和端口设置为,关闭服务端后可立即复用
int opt = 1;
setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

2.地址转换函数

0.IP地址的表现形式

  • 字符串IP:类似于127.0.0.1这种字符串形式的IP地址,叫做基于字符串的点分十进制IP地址
  • **整数IP:**IP地址在进行网络传输时所用的形式,用一个32位的整数来表示IP地址

1.字符串IP转整数IP

  • 功能:一次性完成以下两件事
    1. 将点分十进制字符串风格ip地址 --> 4字节
    2. 4字节主机序列 --> 网络序列
  • 原型:in_addr_t inet_addr(const char *strptr);
    • **参数:strptr:**待转换的字符串IP
    • **返回值:**成功返回转换后的整数IP,失败返回INADDR_NONE(-1)
  • 原型:int inet_aton(const char* strptr, struct in_addr* addrptr);
    • 参数:
      • **strptr:**待转换的字符串IP
      • **addrptr:**输出型参数,存放着转换后的整数IP
    • **返回值:**成功返回非0,失败返回0
  • 原型:int inet_pton(int family, const char *strptr, void *addrptr);
    • 参数:
      • family**:**协议家族
      • strptr**:**待转换的字符串IP
      • addrptr**:**输出型参数,存放着转换后的整数IP
    • **返回值:**成功返回1,失败返回0

2.整数IP转字符串IP

  • 功能:4字节的网络序列的IP --> 本主机的点分十进制字符串风格的IP,方便显示
  • *原型:char inet_ntoa(struct in_addr addrptr)
    • **参数:addrptr:**待转换的整数IP
    • **返回值:**成功返回转换后的字符串IP
  • 原型:const char *inet_ntop(int family, const void *src, char *dest, size_t len);
    • 参数:
      • family**:**协议家族
      • src**:**待转换的整数IP
      • dest**:**输出型参数,转换后的字符串IP
      • len**:**用于指明strptr中可用的字节数
  • 返回值:
    • 成功,返回指向dest的非空指针
    • 失败,返回nullptr

3.关于inet_ntoa()

  • 这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果

    • 那么是否需要调用者手动释放呢?
      • man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放
        请添加图片描述
  • 那么问题来了,如果多次调用这个函数,会有什么样的效果呢?

    • 结果如下
    • 因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果
int main()
{
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    char *ptr1 = inet_ntoa(addr1.sin_addr);
    char *ptr2 = inet_ntoa(addr2.sin_addr);
    printf("ptr1:%s, ptr2:%s\n", ptr1, ptr2);
    return 0;
}
result:ptr1:255.255.255.255, ptr2:255.255.255.255
  • 如果有多个线程调用 inet_ntoa,是否会出现异常情况呢?
    • 在APUE中,明确提出inet_ntoa不是线程安全的函数
    • 在多线程环境下,推荐使用inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题

4.注意:

  • 最常用的两个转换函数是inet_addrinet_ntoa,因为这两个函数足够简单
    • 这两个函数的参数就是需要转换的字符串IP或整数IP,而这两个函数的返回值就是对应的整数IP和字符串IP。
  • 其中inet_ptoninet_ntop函数不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此这两个函数中对应的参数类型是void*

3.使用函数

1.sendto()

  • 功能:UDP下,客户端/服务器发送数据
  • 原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • 参数:
    • **sockfd:**对应操作的文件描述符,表示从该文件描述符索引的文件当中读取数据
    • **buf:**待写入数据的存放位置
    • **len:**期望写入数据的字节数
    • **flags:**写入的方式,一般设置为0,表示阻塞写入
    • **dest_addr:**输出型参数,对端网络相关的属性信息
    • **addrlen:**传入dest_addr结构体的长度
  • **返回值:**成功返回实际写入的字节数,失败返回-1.同时错误码被设置

2.recvfrom()

  • 功能:UDP下,客户端/服务器读取数据
  • 原型:ssize_t recvfrom(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *src_addr, socklen_t *addrlen);
  • 参数:
    • **sockfd:**对应操作的文件描述符,表示从该文件描述符索引的文件当中读取数据
    • **buf:**读取数据的存放位置
    • **len:**期望读取数据的字节数
    • **flags:**读取的方式,一般设置为0,表示阻塞读取
    • **src_add:**输出型参数,对端网络相关的属性信息
    • **addrlen:**输入输出型参数,调用时传入期望读取的src_addr结构体长度,返回时代表实际读到的src_addr结构体的长度
  • **返回值:**成功返回实际读到的字节数,失败返回-1,同时错误码被设置

3.send() && write()

  • 功能:TCP下,客户端/服务器发送数据
  • 原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • 参数:
    • **sockfd:**对应操作的文件描述符,表示从该文件描述符索引的文件当中读取数据
    • **buf:**待写入数据的存放位置
    • **len:**期望写入数据的字节数
    • **flags:**读取的方式,一般设置为0,表示阻塞读取
  • **返回值:**成功返回实际写入的字节数,失败返回-1,同时错误码被设置

4.recv() && read()

  • 功能:TCP下,客户端/服务器读取数据
  • 原型:ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 参数:
    • **sockfd:**对应操作的文件描述符,表示从该文件描述符索引的文件当中读取数据
    • **buf:**读取数据的存放位置
    • **len:**期望读取数据的字节数
    • **flags:**读取的方式,一般设置为0,表示阻塞读取
  • 返回值:
    • 如果返回值大于0,则表示本次实际读取到的字节个数
    • 如果返回值等于0,则表示对端已经把连接关闭了
    • 如果返回值小于0,则表示读取时遇到了错误

5.popen()

  • 功能:
    • 创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据
    • 执行结果可以通过FILE*指针进行读取
  • 原型:FILE *popen(const char *command, const char *type);
  • 参数:
    • command:一个指向以NULL结束的shell命令字符串指针,这行命令将被传到bin/sh并使用**-c**标志,shell将执行这个命令
    • type:只能是读和写的一种
      • 如果是"r"则文件指针连接到command的标准输出,则返回的文件指针是可读的
      • 如果是"w"则文件指针连接到command的标准输入,则返回的文件指针是可写的
  • **返回值:**成功返回文件指针,失败返回nullptr
  • **原理:**创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell命令,然后等待命令终止
    • 执行command --> pipe() fork() 子进程执行(exec)*

6.pclose()

  • 原型:int pclose(FILE *stream);
  • **参数:stream:**popen()返回的文件指针

4.实际使用时注意事项

1.INADDR_ANY

  • 给服务器设置IP时,若手动设置,就代表绑定一个特定的网卡(一个网卡就会有一个IP地址)
    • 如果绑定了一个具体的网卡地址,那么就只监听这个网卡
  • 如果给IP设置为INADDR_ANY(0.0.0.0),则可以监听从本地任意一块网卡来的数据
    • 另外,代码的可移植性高,因为具体的网卡地址,换一个机器就得要变化,0.0.0.0没有这个烦恼
  • 所以一般情况下,除非就是需要设定某个特定IP,否则设置INADDR_ANY就是一个比较好的选择,可参考以下代码
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

2.sockaddr初始化

  • 将整个结构清零
  • 设置地址类型为AF_INET
  • 设置IP和PORT
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

3.客户端需不需要bind?

  • 需要,但客户端不需要用户自己显式手动bind端口信息,OS会自动选择并分配
  • 如果非要自己手动bind,会发生什么? – 会出现以下两种情况
    1. 此端口正好没有别的客户端bind,那么此端口被调用bind的客户端占有,以后其他客户端无法占用此端口
    2. 其他的客户端提前占用了这个端口,因为手动bind端口是被绑定死的,则客户端无法启动
  • 当客户端首次发消息给服务器的时候,,OS会自动给客户端bind它的IP和端口

4.connected socket vs listening socket

  • **listening socket:**一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命期内一直存在
    • 没有关联到客户端连接,而是继续保持监听状态并接受其他连接请求
  • connected socket:
    • listening socket具有相同的套接字类型和地址族
    • 内核为每个由服务器进程接受的客户连接创建一个已连接套接字
    • 当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭
    • 该描述符连接到调用connect的客户端
    • 所以我也更愿意称之为**“service socket”**
  • 总结:
    • listening socket只获取连接,获取到连接后,他就不再管了,而是继续去获取新连接
    • connected socket是那个真正为客户端提供服务的文件描述符

5.TCP协议通讯流程

0.基于TCP协议的客户端/服务器程序的一般流程

请添加图片描述

1.服务器初始化

  • 调用socket,创建文件描述符
  • 调用bind,将当前的文件描述符和ip/port绑定在一起;如果这个端口已经被其他进程占用了,就会bind失败
  • 调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备
  • 调用accecpt,并阻塞,等待客户端连接过来

2.客户端建立连接的过程

  • 调用socket,创建文件描述符
  • 调用connect,向服务器发起连接请求
  • connect会发出SYN段并阻塞等待服务器应答(第一次)
  • 服务器收到客户端的SYN,会应答一个SYN-ACK段表示"同意建立连接"(第二次)
  • 客户端收到SYN-ACK后会从connect()返回,同时应答一个ACK段(第三次)
  • 这个建立连接的过程,通常称为三次握手

3.数据传输的过程

  • 建立连接后,TCP协议提供全双工的通信服务
    • 所谓全双工的意思是,在同一条连接中,同一时刻,通信双方可以同时写数据
    • 相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据
  • 服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待
  • 这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答
  • 服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求
  • 客户端收到后从read()返回,发送下一条请求,如此循环下去

4.断开连接的过程

  • 如果客户端没有更多的请求了,就调用close()关闭连接,客户端会向服务器发送FIN段(第一次)
  • 此时服务器收到FIN后,会回应一个ACK,同时read会返回0(第二次)
  • read返回之后,服务器就知道客户端关闭了连接,也调用close关闭连接,这个时候服务器会向客户端发送一个FIN(第三次)
  • 客户端收到FIN,再返回一个ACK给服务器(第四次)
  • 这个断开连接的过程,通常称为四次挥手

5.TCP vs UDP

  • 可靠传输 vs 不可靠传输
  • 有连接 vs 无连接
  • 字节流 vs 数据报

6.其他

1.netstat

  • 运行服务端后,可以通过netstat命令来查看当前网络的状态,常用选项如下

    -n直接使用ip地址,而不通过域名服务器
    -l显示监控中的服务器的socket
    -t显示TCP传输协议的连线情况
    -u显示UDP传输协议的连线情况
    -p显示正在使用socket的程序识别码和程序名称
  • 34
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

DieSnowK

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

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

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

打赏作者

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

抵扣说明:

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

余额充值