socket编程之send()函数族

8.send()函数族

系统调用send、sendto(), and sendmsg()用于将消息发送给另一个socket。

8.1 包含头文件
#include <sys/types.h>
#include <sys/socket.h>		
8.2 函数主体
1.ssize_t send(int sockfd, const void *buf, size_t len, int flags);
2.ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                 const struct sockaddr *dest_addr, socklen_t addrlen);
3.ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

参数解释:

  • int sockfd

​ 发送套接字的文件描述符

  • void *buf

​ send()函数簇读到的数据将保存到buf所指向的空间中,用法和write()同理。

  • size_t len

​ 该参数指明本次调用接收数据的最大长度。

  • int flags

​ 如果flags设置为0,那么send()和write()没有任何区别。send()只用于面向连接的socket,即收方IP是知道的,flags还可以通过OR设置如下参数:

标志位解释
MSG_CONFIRM只在SOCK_DGRAM和SOCK_RAW类型的socket有效,该标志位用于通知链路层收到了成功回复,以避免链路层定期发送ARP协议,减少流量。
MSG_DONTROUTE不使用网关发送数据包,只能发送到直接连接的网络上的主机,通常用于网络诊断的路由系列协议。
MSG_DONTWAIT设置本次调用为非阻塞,与O_NONBLOCK flag类似,但是该标志为作用域仅限于本次send调用,而后者则是对文件描述符操作。
MSG_MORE此标志指定会有更多的数据发送,与 TCP_CORK 套接字设置效果相同,此时内核将保留这些数据,仅在下一次调用未指定该标志时进行传输。
MSG_NOSIGNAL此标志指定调用send(),当对端的socket已经关闭时,不会产生SIGPIPE信号,但仍会返回EPIPE错误,作用范围为本次调用。这与sigaction忽略 SIGPIPE信号效果相同,但是后者会影响整个线程。
MSG_OOB此标志指定在正常数据流的接受中不会接收带外数据。某些协议将加急数据放在正常数据队列的头部,因此此标志不能与此类协议一起使用。

这里对flag:MSG_CONFIRM稍做补充:

根据IOS五层模型,网络层将IP数据包封装好后会经过数据链路层封装源MAC和目的MAC,而ARP缓存表中则存放有IP到MAC的映射,主机通过ARP缓存表可以查找到目的MAC。但当ARP缓存表中没有对应的表项时,主机会以广播的方式向局域网中发送ARP请求,目的主机则会以单播的方式将自己的MAC封装到APR回文报文中,这样本主机便可知道目的MAC,当两台主机不在一个网段时,则需要通过路由器转发,参考arp请求跨网段工作原理

在LINUX内核中设置了ARP缓存的老化时间机制,用于管理ARP缓存表,MSG_CONFIRM标志位则用于通知链路层继续保持Reachable状态,而不需要在发送ARP请求报文来确认是否更新当前状态,减少了ARP报文在网络中传输,具体细节需要时在做补充,参考《Linux实现的ARP缓存老化时间原理解析》。

  • const struct sockaddr *dest_addr

​ 如果dest_addr非NULL,且协议提供消息的地址信息,则该目标地址将放在 dest_addr所指向的缓冲区中。

  • socklen_t *addrlen

    addrlen 是一个value-result参数。在调用之前,应将其初始化为与dest_addr关联的缓冲区的大小。返回后,addrlen 将更新为包含源地址的实际大小。 如果提供的缓冲区太小,则返回的地址将被截断;在这种情况下,addrlen 将返回一个大于提供给调用的值。

    如果不需要知道源地址,则可以将dest_addr和addrlen均设为NULL即可,此时以下两个调用等价,send(sockfd, buf, len, flags) 与sendto(sockfd, buf, len, flags, NULL, 0);。

  • struct msghdr *msg

//该结构体如下,等用到时,在进行补充
  struct iovec {                    /* Scatter/gather array items */
               void  *iov_base;              /* Starting address */
               size_t iov_len;               /* Number of bytes to transfer */
           };
 struct msghdr {
               void         *msg_name;       /* optional address */
               socklen_t     msg_namelen;    /* size of address */
               struct iovec *msg_iov;        /* scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* ancillary data, see below */
               size_t        msg_controllen; /* ancillary data buffer len */
               int           msg_flags;      /* flags on received message */
           };
8.3 返回值

这些调用返回收到的字节数,如果发生错误,则返回 -1,如果消息不适合套接字的发送缓冲区时,send()通常会阻塞(未设置非阻塞时)。如果发生错误,设置了 errno 用来指示错误。错误类型如下(只列出socket层产生的错误,更底层(传输层及以下)的错误再前面文章种已经全部列出):

错误类型解释
EBADFsocket并非有效的文件描述符
ECONNREFUSED对端主机拒绝了网络连接
EFAULTbuff缓冲区指向非法内存
EINTR在接收数据之前,本次调用被信号中断
EINVAL传递了无效参数
ENOTCONNsocket为面向连接套接字,但没有成功连接
ENOTSOCKsockfd引用的并不是一个socket
ENOMEM无可用内存
EMSGSIZE套接字类型要求以原子方式发送消息,而要发送的消息太大而不可能实现。
EISCONN套接字以建立连接,但是又重新指定了接收端信息
ECONNRESET连接已被对方重置
8.4 结束

当用户调用recv()/send()后,数据会读/写到指定套接字的读/写缓冲区,这里需要注意的是,send、write系统调用只负责将数据存到指定的缓存区,便成功返回,而将数据传输到对端主机中的过程则是由内核调用TCP/IP协议完成,此过程则不由用户控制,send函数也不会返回对用的状态信息。在调试程序时,可以通过“netstat -an”命令查看socket读写缓存区中是否由未发送的数据。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现ping命令的关键在于向目标主机发送ICMP数据包并接收回复。以下是一个使用socket编程实现ping命令的Python代码示例: ```python import os import socket import struct import select import time ICMP_ECHO_REQUEST = 8 # ICMP类型码,代表Echo请求 def checksum(data): """计算ICMP数据包的校验和""" if len(data) % 2 == 1: data += b'\x00' words = struct.unpack('!%sH' % (len(data) // 2), data) sum_ = sum(words) sum_ = (sum_ >> 16) + (sum_ & 0xffff) sum_ += sum_ >> 16 return ~sum_ & 0xffff def send_ping(sock, dest_addr, pid, seq): """向目标主机发送一个Echo请求""" checksum_ = 0 # 构造ICMP头部 header = struct.pack('bbHHh', ICMP_ECHO_REQUEST, 0, checksum_, pid, seq) # 构造ICMP数据 data = b'Hello, World!' checksum_ = checksum(header + data) header = struct.pack('bbHHh', ICMP_ECHO_REQUEST, 0, socket.htons(checksum_), pid, seq) packet = header + data # 发送数据包并记录发送时间 start_time = time.time() sock.sendto(packet, (dest_addr, 1)) return start_time def receive_ping(sock, pid, seq, timeout): """从socket中接收一个Echo回复""" time_left = timeout while True: start_time = time.time() # 使用select等待可读socket ready = select.select([sock], [], [], time_left) if not ready[0]: return None # 从socket中读取数据 recv_packet, addr = sock.recvfrom(1024) # 解析ICMP头部 icmp_header = recv_packet[20:28] type_, code, checksum_, recv_pid, recv_seq = struct.unpack('bbHHh', icmp_header) # 如果收到的是Echo回复,则返回接收时间 if type_ == 0 and code == 0 and recv_pid == pid and recv_seq == seq: return time.time() - start_time # 更新等待时间 time_left -= time.time() - start_time if time_left <= 0: return None def ping(dest_addr, count=4, timeout=2): """向目标主机发送多个Echo请求,并打印结果""" pid = os.getpid() seq = 0 # 创建ICMP socket sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) for i in range(count): seq += 1 # 发送Echo请求并记录发送时间 start_time = send_ping(sock, dest_addr, pid, seq) # 接收Echo回复并记录接收时间 receive_time = receive_ping(sock, pid, seq, timeout) # 打印结果 if receive_time is None: print(f'Ping {dest_addr} ({i+1}/{count}): Timeout') else: print(f'Ping {dest_addr} ({i+1}/{count}): {round(receive_time*1000)}ms') # 等待一段时间后发送下一个Echo请求 time.sleep(1) sock.close() ``` 使用示例: ```python ping('www.baidu.com') ``` 输出: ``` Ping www.baidu.com (1/4): 22ms Ping www.baidu.com (2/4): 23ms Ping www.baidu.com (3/4): 22ms Ping www.baidu.com (4/4): 22ms ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值