Linux 系统编程 学习:07-基于socket的网络编程2:基于 UDP 的通信
阅读原文时间:2021年05月28日阅读:9

Linux 系统编程 学习:07-基于socket的网络编程2:基于 UDP 的通信

上一讲我们介绍了网络编程的一些概念。socket的网络编程的有关概念

这一讲我们来看UDP 通信。

UDP:User Datagram Protocol的缩写。

UDP不提供复杂控制机制,利用IP提供面向无连接的通信服务。且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。

UDP面向无连接,可以随时发送数据。它常用于几个方面:

  • 包总量较少的通信(DNS、SNMP等)
  • 视频、音频等多媒体通信(即时通信)
  • 限定于LAN等特定网络中的应用通信
  • 广播通信(广播、多播)

典型的 UDP 通信流程图如下:

%% 时序图
sequenceDiagram
participant Server
participant Client

%% Note right of Client: Client主动连接->
Note right of Server: 双方都创建socket对象

Server --> Server: socket()
Client --> Client: socket()

Note left of Server: 服务器一般绑定端口号
Server --> Server: bind()

Note right of Server: 收发消息
Client -->> Server: sendto()/recvfrom()
Server -->> Client: sendto()/recvfrom()

Note right of Server: 关闭连接

Client --> Client: close()
Server --> Server: close()

根据流程图,我们知道,在UDP通信中,使用到了这些函数:socket()bind()sendto()recvfrom()

socket

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

描述 :创建一个用于网络通信的套接字。

参数解析

domain:

  • AF_INET:IPv4协议
  • AF_INET6:IPv6协议
  • AF_LOCAL:Unix域协议
  • AF_ROUTE:路由套接口
  • AF_KEY:密钥套接口

在早期版本中的domain参数中还有以 PF_开头的定义,当初设计者设计的时候,设想地址使用一套域,协议使用一套域,然后就没有然后了。所以,实际上,PF_ 其实相当于 AF_

type:

  • SOCK_STREAM:提供面向连接的双向可靠数据流,对应TCP协议

  • SOCK_DGRAM:提供无保障的面向消息的双向不可靠数据报,对应UDP,可用于在网络上发广播信息。(UDP)

  • SOCK_RAW:提供传输层以下的协议,可以访问内部网络接口,例如接收和发送ICMP报文

  • SOCK_RDM:提供可靠的数据报文,但可能数据会有乱序

  • SOCK_SEQPACKET:序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出

  • 同时,可以 与下面的值相或

    • SOCK_NONBLOCK . 非阻塞,也可以使用 fcntl()来做这个事情
    • SOCK_CLOEXEC 在执行 exec 时关闭该描述符,相当于 FD_CLOEXEC

protocol:当type为SOCK_RAW时需要设置此值说明协议类型,其他类型设置为0即可, 对于protocol为0(IPPROTO_IP)的raw socket。用于接收任何的IP数据包。其中的校验和和协议分析由程序自己完成

返回值: 成功返回socket描述符;失败返回-1,置errno:

底层协议模块可能会产生其他错误。

  • ERRORS:创建指定类型和/或pro-tocol的套接字的权限被拒绝
  • EAFNOSUPPORT:不支持指定的地址族
  • EINVAL 未知协议,或协议系列不可用
  • EINVAL :无效的type
  • EMFILE :已达到每个进程对打开的文件描述符数的限制
  • ENFILE :已达到系统范围内打开文件总数的限制
  • ENOBUFS or ENOMEM :可用内存不足。只有释放足够的资源,才能创建套接字
  • EPROTONOSUPPORT :此域中不支持协议类型或指定的协议

bind

一个套接字只是用户程序与内核交互信息的枢纽,它自身没有太多的信息,也没有网络协议地址和端口号等信息。进行网络通信时,必须把一个套接字与一个地址相关联,这个过程就是地址绑定的过程。

许多时候内核会我们自动绑定一个地址,然而有时用户可能需要自己来完成这个绑定的过程,以满足实际应用的需要。

最典型的情况是一个服务器进程需要绑定一个众所周知的地址或端口以等待客户来连接。这个事由bind的函数完成。

服务器启动时需要绑定指定的端口来提供服务(以便于客户向指定的端口发送请求),对于服务器socket绑定地址,一般而言将IP地址赋值为INADDR_ANY(该宏值为0),即无论发送到系统中的哪个IP地址(当服务器有多张网卡时会有多个IP地址)的请求都采用该socket来处理,而无需指定固定IP。

对于Client,一般而言无需主动调用bind(),一切由操作系统来完成。在发送数据前,操作系统会为套接字随机分配一个可用的端口,同时将该套接字和本地地址信息绑定。

一个网络应用程序只能绑定一个端口( 一个套接字只能 绑定一个端口 )。如果需要绑定多个端口,需要使用端口复用(略)。

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

/* 可能会用到以下结构体 */
struct sockaddr {
   sa_family_t sa_family; // 地址族
   char        sa_data[14]; // 数据
}

/* 一般使用以下结构体代替 */
#include <netinet/in.h>
#include <arpa/inet.h>
struct sockaddr_in {
    sa_family_t     sa_family;   // 地址族
    uint16_t        sin_port;    // 16位 TCP/UDP 端口号
    struct in_addr     sin_addr;    // 32位IP地址,里面的内容是 In_addr_t
    char            sin_zero[8]; // 不使用
}

描述: 将addr指向的sockaddr结构体中描述的一些属性(IP地址、端口号、地址簇)与socket套接字绑定(也叫给套接字命名)。 为socket套接字关联了一个相应的地址与端口号,即发送到地址值该端口的数据可通过socket读取和使用。

参数解析

sockfd:由socket()函数成功返回的结果

addr: 设置有网络属性的sockaddr_in或sockaddr 结构体

  • sa_family

addrlen:sockaddr_in或sockaddr 结构体的大小

与 sockaddr_in 有关的函数

常在端口号中使用的:

#include <arpa/inet.h>

// 主机字节序到网络字节序
u_long htonl (u_long hostlong);
u_short htons (u_short short);

// 网络字节序到主机字节序
u_long ntohl (u_long hostlong);
u_short ntohs (u_short short);

常在地址转换中使用的:

#include <arpa/inet.h>

// 将网络地址转换为 以 点分十进制表示的字符串(字符串存在于静态内存中)
char *inet_ntoa(struct in_addr);

// 点分十进制字符串 转 32位(IPV4 IP)二进制
in_addr_t inet_addr(const char* cp);

范例:

    ...
    struct sockaddr_in addr ={0};
    addr.sin_family = AF_INET;       // 地址协议族
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
    addr.sin_port = htons(12345); //指定端口号
    ...

sendto()

关于发送,有下列这些函数。

send只可用于基于连接(TCP)的套接字,send 和 write唯一的不同点是标志的存在,当标志为0时,send等同于write。

sendto 和 sendmsg既可用于无连接的套接字,也可用于基于连接的套接字。除了套接字设置为非阻塞模式,调用将会阻塞直到数据被发送完。

#include <sys/types.h>
#include <sys/socket.h>

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

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

/* 可能用到的结构体 */
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 (unused) */
};

描述:发送数据到指定的地址

参数解析:

sockfd:要发送到哪个套接字

buf:要发送数据首地址

len:要发送的数据长度

flags:是以下零个或者多个标志的相或结果

  • MSG_DONTROUTE:不要使用网关来发送封包,只发送到直接联网的主机。这个标志主要用于诊断或者路由程序。

  • MSG_DONTWAIT:操作不会被阻塞。

  • MSG_EOR:终止一个记录。

  • MSG_MORE:调用者有更多的数据需要发送。

  • MSG_NOSIGNAL:当另一端终止连接时,请求在基于流的错误套接字上不要发送SIGPIPE信号。

  • MSG_OOB:发送out-of-band数据(需要优先处理的数据),同时现行协议必须支持此种操作。

    传输层协议使用带外数据(out-of-band,OOB)来发送一些重要的数据,如果通信一方有重要的数据需要通知对方时,协议能够将这些数据快速地发送到对方。

    为了发送这些数据,协议一般不使用与普通数据相同的通道,而是使用另外的通道。linux系统的套接字机制支持低层协议发送和接受带外数据。但是TCP协议没有真正意义上的带外数据。为了发送重要协议,TCP提供了一种称为紧急模式(urgent mode)的机制。TCP协议在数据段中设置URG位,表示进入紧急模式。接收方可以对紧急模式采取特殊的处理。很容易看出来,这种方式数据不容易被阻塞,并且可以通过在我们的服务器端程序里面捕捉SIGURG信号来及时接受数据。

dest_addr:目的地址,可为空

addrlen:地址属性的长度(dest_addr的大小,其空则为0)

msg:(sendmsg、recvmsg函数使用) 有关内容请参考 《socket编程:recvmsg 和 sendmsg 函数》

recvfrom()

recvfrom 会返回发送端的地址,这样对服务器来说,由于时 UDP socket 对象没有记录对应的IP和端口信息(记录也没有用,UDP不稳定,随时可能变化),会需要用到该地址给客户端来发送响应。对于客户端,由于每次始终是知道服务器IP地址和端口(和一个服务器交互),所以无需记录(除非UDP客户端需要和多个服务器交互,需要一一记录,才能确保交互正确)

#include <sys/types.h>
#include <sys/socket.h>

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

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

描述: 从一个套接口接收消息

recvfromrecvmsg()用来从一个套接口接收消息,也可以用来在一个面向连接或非连接的套接口上接收数据。

recv调用通常只用于已建立连接的套接口,等效于参数src_addr为NULL的recvfrom调用。

close() 与 shutdown()

#include <unistd.h>

int close(int fd);

#include <sys/socket.h>

int shutdown(int sockfd, int how);

close 可以用来关闭一个socket。

Linux的close函数和Windows的closesocket函数意味着完全断开连接,完全断开不仅指无法传输数据,而且也不能接收数据。在某些情况下,通信一方调用close或closesocket函数断开连接就显得不太优雅。 ( 只要对方能收到FD_CLOSE,而不是傻等在循环里,应该都算优雅吧。)

本端程序想发送的数据都已经投放到发送缓冲区内了,等待对方收到数据后,对方主动关闭连接,本方检测到对方套接字关闭事件后,被动的关闭自己的连接。

1.注册FD_CLOSE的通知。

2.把所有要发送的数据都发送完毕后,调用shutdown(s, SD_SEND)。

3.sleep一会,或者阻塞调用recv知道他返回0或socket_error,然后调用closesocket。

但是,网上还有很多资料说,调用setsockopt设置so_linger选项时,如果设置了SO_DONTLINGER,或者SO_LINGER并且间隔非0,也属于优雅的关闭。这种情况下,tcp在调用closesocket时,也会在关闭套接字资源之前,尽力的将发送缓冲区内的数据发送出去。看了一下MSDN,基本上也是这么解释的。

为了解决这类问题,“只关闭一部分数据交换中使用的流”的方法应运而生。断开一部分连接是指,可以传输数据但无法接收,或可以接收数据但无法传输。即只关闭流的一半。

参数解析:

sock:需要断开的套接字文件描述符。

how:传递断开方式信息

  • SHUT_RD:断开输入流。
  • SHUT_WR:断开输出流。
  • SHUT_RDWR:同时断开I/O流。(不允许进一步接收和传输)

返回值: 成功返回0,失败返回-1。

我们在2个非亲缘进程中实现一收一发的通信。要求先启动服务器程序,客户端随后启动。

client.c

/*
#    Copyright By Schips, All Rights Reserved
#    https://gitee.com/schips/
#
#    File Name:  client.c
#    Created  :  Sat 21 Mar 2020 04:43:39 PM CST
*/

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

typedef struct _info {
    char name[10];
    char text[54];
}info;

int main(int argc, char *argv[])
{
    int my_socket;
    unsigned int len;

    // 创建套接字
    my_socket = socket(AF_INET, SOCK_DGRAM, 0);// IPV4, UDP socket
    if(my_socket == -1) { perror("Socket"); }
    printf("Creat a socket :[%d]\n", my_socket);

    // 定义发送的消息
    info buf ={0};
    sprintf(buf.name, "schips");
    sprintf(buf.text, "Socket send");

    // 指定地址
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;  // 地址协议族
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
        addr.sin_port = htons(12345); //指定端口号

    // 发送消息
    sendto(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, sizeof(struct sockaddr_in));
        perror("sendto");

    // 接收服务器的消息
    recvfrom(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, &len);
        perror("recvfrom");

    printf("%s\n", buf.name);
    printf("%s\n", buf.text);

    // 关闭连接
    //shutdown(my_socket, SHUT_RDWR); perror("shutdown");
    //printf("%d\n", errno);
    return close(my_socket); perror("close");
    printf("%d\n", errno);
    return errno;
}

server.c

/*
#    Copyright By Schips, All Rights Reserved
#    https://gitee.com/schips/
#
#    File Name:  server.c
#    Created  :  Sat 21 Mar 2020 04:43:39 PM CST
*/

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

typedef struct _info {
    char name[10];
    char text[54];
}info;

int main(int argc, char *argv[])
{
    int my_socket;
    unsigned int len;

    // 创建套接字
    my_socket = socket(AF_INET, SOCK_DGRAM, 0);// IPV4, UDP socket
    if(my_socket == -1) { perror("Socket"); }
    printf("Creat a socket :[%d]\n", my_socket);

    // 用于接收消息
    info buf ={0};

    // 指定地址
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;  // 地址协议族
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
        addr.sin_port = htons(12345); //指定端口号

    // 服务器 绑定
    bind(my_socket, (struct sockaddr *)&addr, sizeof(addr));

    // 接收并打印消息
    //recvfrom(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, &len);
    recvfrom(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, &len);
        perror("recvfrom");

    printf("%s\n", buf.name);
    printf("%s\n", buf.text);

    // 回复消息
    sprintf(buf.name, "Server");
    sprintf(buf.text, "Had recvied your message");
    sendto(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, sizeof(struct sockaddr_in));
        perror("sendto");

    // 关闭连接
    //shutdown(my_socket, SHUT_RDWR); perror("shutdown");
    //printf("%d\n", errno);
    return close(my_socket); perror("close");
    printf("%d\n", errno);
    return errno;
}