对码当歌,猿生几何?

UDP&TCP Linux网络应用编程详解

1.目标

暂时想不出什么好的应用场景,
目前想到目标就是实现让两个设备通过网络传输数据,
比如开发板和Linux主机之间传数据,
以后就可以实现开发板通过网络上报数据或者主机通过网络控制开发板

此外,暂时不想关心具体的网络模型,更注重于网络相关函数的直接使用。

2.Linux网络编程基础

2.1 嵌套字

多个TCP连接或者多个应用程序进程 可能需要同一个TCP端口传输数据。
为了区分不同应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP交互提供了称为嵌套字(Socket)的接口。
Linux中的网络编程正是通过Socket接口实现的,Socket是一种文件描述符。

常用的TCP/IP有以下三种类型的嵌套字:
- 流式嵌套字(SOCK_STREAM)
用于提供面向连接的、可靠的数据传输服务,即使用TCP进行传输。
- 数据报嵌套字(SOCK_DGRAM)
用于提供无连接的服务,即使用UDP进行传输。
- 原始嵌套字(SOCK_RAW)
可以读写内核没有处理的IP数据报,而流式嵌套字只能读取TCP的数据,数据报嵌套字只能读取UDP的数据。

因此,如果要访问其它协议发送的数据必须使用原始嵌套字,它允许对底层协议(如IP或ICMP)直接访问。

2.2 端口

TCP/IP协议中的端口,端口号的范围从0~65535。
一类是由互联网指派名字和号码公司ICANN负责分配给一些常用的应用程序固定使用的“周知的端口”,其值一般为0~1023。例如http的端口号是80,FTP为21,SSH为22,Telnet为23等。
还有一类是用户自己定义的,通常是大于1024的整型值。

2.3 网络地址

网络通信,归根到底还是进程间的通信(不同计算机上的进程间通信)。
在网络中,每一个节点(计算机或路由)都有一个网络地址,如192.168.1.4,也就是IP地址。
两个进程通信时,首先要确定各自所在的网络节点的网络地址。

但是,网络地址只能确定进程所在的计算机,而一台计算机上很可能同时运行着多个进程,所以仅凭网络地址还不能确定到底是和网络中的哪一个进程进行通信,因此套接口中还需要包括其他的信息,也就是端口号(PORT)。
在一台计算机中,一个端口号一次只能分配给一个进程,也就是说,在一台计算机中,端口号和进程之间是一一对应关系。

所以,使用端口号和网络地址的组合可以唯一的确定整个网络中的一个网络进程

例如,如网络中某一台计算机的IP为192.168.1.4,操作系统分配给计算机中某一应用程序进程的端口号为1500,则此时192.168.1.4 1500就构成了一个套接口。

2.3网络地址的格式

在Socket程序设计中,struct sockaddr用于记录网络地址,其格式如下:

struct sockaddr
{     unsigned short sa_family; /*协议族,采用AF_XXX的形式,例如AF_INET(IPv4协议族)*/ char sa_data[14]; /*14字节的协议地址,包含该socket的IP地址和端口号。*/};

但在实际编程中,并不针对sockaddr数据结构进行操作,而是用与其等价的sockaddr_in数据结构:

struct sockaddr_in
{     short int sa_family; /*地址族*/ unsigned short int sin_port; /*端口号*/ struct in_addr sin_addr; /*IP地址*/ unsigned char sin_zero[8]; /*填充0 以保持与struct sockaddr同样大小*/};

2.3.1 网络地址的转换

IP地址通常用数字加点(如192.168.1.a)表示,而在struct in_addr中使用的式32位整数表示。因此,Linux提供如下函数进行两者之间的转换:
- inet_aton()函数:

所需要头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
函数格式
int inet_aton(const char *cp, struct in_addr *inp);
函数功能
 将a.b.c.d字符串形式的IP地址转换成32位网络序号IP地址;
 *cp:存放字符串形式的IP地址的指针
 *inp:存放32位的网络序号IP地址
返回值
 转换成功,返回非0,否则返回0;

 


  • inet_ntoa()函数:客户机端:

 

所需要头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
函数格式
char *inet_ntoa(struct in_addr in);
函数功能
 将32位网络序号IP地址转换成a.b.c.d字符串形式的IP地址;
 in:Internet主机地址的结构
返回值
 转换成功,返回一个字符指针,否则返回NULL;

2.4 字节序

不同的CPU采用对变量的字节存储顺序可能不同。
常用的X86结构是小端模式,很多的ARM,DSP都为小端模式,即内存的低地址存储数据的低字节,高地址存储数据的高字节。
而KEIL C51则为大端模式,内存的高地址存储数据的低字节,低地址存储数据高字节。

对于网络传输来说,数据顺序必须是一致的,网络字节顺序采用大端字节序方式。
下面是四个常用的转换函数:

主机转网络:
- htons()函数:

所需要头文件
#include <netinet/in.h>
函数格式
unsigned short int htons(unsigned short int hostshort)
函数功能
 将参数指定的16位主机(host)字符顺序转换成网络(net)字符顺序;
 hostshort:待转换的16位主机字符顺序数
返回值
 返回对应的网络字符顺序数;

 


  • htonl()函数:

 

所需要头文件
#include <netinet/in.h>
函数格式
unsigned long int htons(unsigned long int hostlong)
函数功能
 将参数指定的32位主机(host)字符顺序转换成网络(net)字符顺序;
 hostlong:待转换的32位主机字符顺序数
返回值
 返回对应的网络字符顺序数;


网络转主机:
- ntohs()函数:

所需要头文件
#include <netinet/in.h>
函数格式
unsigned short int ntohs(unsigned short int netshort)
函数功能
 将参数指定的16位网络(net)字符顺序转换成主机(host)字符顺序;
 netshort:待转换的16位网络字符顺序数
返回值
 返回对应的主机字符顺序数;

 


  • ntohl()函数:

 

所需要头文件
#include <netinet/in.h>
函数格式
unsigned long int ntohl(unsigned long int netlong)
函数功能
 将参数指定的32位网络(net)字符顺序转换成主机(host)字符顺序;
 netshort:待转换的32位网络字符顺序数
返回值
 返回对应的主机字符顺序数;

3.TCP

TCP有专门的传递保证机制,收到数据时会自动发送确认消息,发送方收到确认消息后才会继续发送消息,否则继续等待。 这样的好处是传输的数据是**可靠**的,此外它是**有连接**的传输,大多数网络传输都是用的TCP。

3.1 TCP流程图

3.2 TCP步骤分析

程序分为服务器端和客户机端,先从服务器端开始分析。
- 服务器端:
a. 创建socket

if (-1 == sock_fd)
{
        fprintf(stderr,"socket error:%sa", strerror(errno));exit(1);
}

所需要头文件
#include <sys/types.h>
#include <sys/socket.h>
函数格式
int socket(int domain, int type, int protocol);
函数功能
 创建一个套接字;
 domain:协议域(族),决定了套接字的地址类型,例如AF_INET决定了要用IPv4地址(32位)与端口号(16位)的组合。常见的协议族有:AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX)、AF_ROUTE等;
 type:指定套接字类型SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、SOCK_RAW
 protocol:指定socket所使用的传输协议编号,通常为0
返回值
 若成功,返回一个套接字描述符,否则返回-1;

Socket就是一种文件描述符,和普通的打开文件一样,需要检测其返回结果。 **b. 设置socket**

    server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY:This machine all IPserver_addr.sin_port = htons(PORT_NUMBER);

设置何种协议族,设置本机IP和端口,也就有了唯一性。 **c. 绑定socket**

    if(-1 == ret){fprintf(stderr,"bind error:%s
a", strerror(errno));close(sock_fd);exit(1);
    }

所需要头文件
#include <sys/types.h>
#include <sys/socket.h>
函数格式
int bind(int sockfd, struct sockaddr *addr, int addrlen);
函数功能
 把套接字绑定到本地计算机的某一个端口上;
 sockfd:待绑定的套接字描述符
 addr:一个struct sockaddr *指针,指定要绑定给sockfd的协议地址。内容结构由前面的协议族决定。
 addrlen:地址的长度
返回值
 若成功,返回0,否则返回-1,错误信息存在errno中;

d. 开始监听

    if (-1 == ret)
    {
        fprintf(stderr,"listen error:%sa", strerror(errno));close(sock_fd);exit(1);
    }

所需要头文件
#include <sys/types.h>
#include <sys/socket.h>
函数格式
int listen(int sockfd, int backlog);
函数功能
 使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求,最大连接数量为backlog≤128;
 sockfd:待监听的套接字描述符
 backlog:最大可监听和连接的客户端数量
返回值
 若成功,返回0,否则返回-1;

e. 阻塞,等待连接

        new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &addr_len);if (-1 == new_fd)
        {
            fprintf(stderr,"accept error:%sa", strerror(errno));close(sock_fd);exit(1);
        }

所需要头文件
#include <sys/types.h>
#include <sys/socket.h>
函数格式
int accept(int sockfd, struct sockaddr *addr, int *addrlen);
函数功能
 接受连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求;
 当accept函数接受一个连接时,会返回一个新的socket标识符,以后的数据传输和读取就要通过这个新的socket编号来处理,原来参数中的socket也可以继续使用,继续监听其它客户机的连接请求;
 accept连接成功时,参数addr所指的结构体会填入所连接机器的地址数据;
 sockfd:待监听的套接字描述符
 addr:指向struct sockaddr的指针,用于返回客户端的协议地址
 addrlen:协议地址的长度
返回值
 若成功,返回一个由内核自动生成的一个全新描述字,代表与返回客户的TCP连接,否则返回-1,错误信息存在errno中;

f. 接收数据

    if (recv_len <= 0)
    {
        fprintf(stderr, "recv error:%sa", strerror(errno));close(new_fd);  exit(1);
    }else{
        recv_buf[recv_len] = '�';printf("Get msg from client%d: %s", client_num, recv_buf);
    }

所需要头文件
#include <sys/types.h>
#include <sys/socket.h>
函数格式
int recv(int sockfd, void *buf, size_t len, int flags);
函数功能
 用新的套接字来接收远端主机传来的数据,并把数据存到由参数buf指向的内存空间;
 sockfd:sockfd为前面accept的返回值,即new_fd,也就是新的套接字
 buf:指明一个缓冲区
 len:指明缓冲区的长度
 flags:通常为0
返回值
 若成功,返回接收到的字节数,另一端已关闭则返回0,否则返回-1,错误信息存在errno中;

g. 关闭socket

    exit(0);

为了应对多个连接,并保证它们之间相互独立,实际编程中往往还要加入多进程fork()。 让子进程接收数据,父进程继续监听新的连接。

  • 客户机端:

a. 创建socket

    if (-1 == sock_fd)
    {
        fprintf(stderr,"socket error:%sa", strerror(errno));exit(1);
    }

b. 设置socket

    server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT_NUMBER);

其中注意的是,这里设置的socket内容是指 希望连接的服务器IP和端口号信息,IP地址来自用户的输入,并转换格式得到。因此,这里的设置和服务器的设置,要保持内容上的一致。

    if(0 == ret){fprintf(stderr,"server_ip error.
");close(sock_fd);exit(1);
    }

c. 连接

    if (-1 == ret)
    {
        fprintf(stderr,"connect error:%sa", strerror(errno));close(sock_fd);exit(1);
    }

所需要头文件
``#include <sys/types.h>
#include <sys/socket.h>
函数格式
int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);
函数功能
 用来请求连接远程服务器,将参数sockfd的socket连至参数serv_addr所指定的服务器IP和端口号上去;
 sockfd:客户端的socket套接字
 serv_addr:一个struct sockaddr类型的结构体指针变量,存储着远程服务器的IP与端口号信息
 addrlen:结构体变量的长度
返回值
 若成功,返回0,否则返回-1,错误信息存在errno中;

d. 发送

            if (send_buf <= 0)
            {
                fprintf(stderr,"send error:%sa", strerror(errno));close(sock_fd);exit(1);
            }

所需要头文件
#include <sys/types.h>
#include <sys/socket.h>
函数格式
int send(int sockfd, const void *buf, int len, int flags);
函数功能
 用来发送数据给指定的远端主机;
 sockfd:客户端的socket套接字
 buf:指明一个缓冲区
 len:指明缓冲区的长度
 flags:通常为0
返回值
 若成功,返回发送的字节数,否则返回-1,错误信息存在errno中

d. 关闭socket

    close(sock_fd);exit(0);

3.3 TCP完整代码

/*
* tcp_server.c
# Copyright (C) 2017 hceng, <huangcheng.job@foxmail.com>
# Licensed under terms of GPLv2
#
# This program is used for TCP / UDP learning.
# https://hceng.cn/
*/#include <stdlib.h>#include <errno.h>#include <sys/types.h>      #include <sys/socket.h>#include <string.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <stdio.h>#include <signal.h>#define PORT_NUMBER 8888#define BACKLOG     10/* socket->bind->listen->accept->send/recv->close*/int main(int argc, char **argv)
{int sock_fd, new_fd;struct sockaddr_in server_addr;struct sockaddr_in client_addr;int ret;int addr_len;int recv_len;unsigned char recv_buf[1000];int client_num = -1;

    signal(SIGCHLD,SIG_IGN);/* socket */sock_fd = socket(AF_INET, SOCK_STREAM, 0);//AF_INET:IPV4;SOCK_STREAM:TCPif (-1 == sock_fd)
    {fprintf(stderr,"socket error:%s
a", strerror(errno));exit(1);
    }/* set server sockaddr_in */memset(&server_addr, 0, sizeof(struct sockaddr_in));//clearserver_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY:This machine all IPserver_addr.sin_port = htons(PORT_NUMBER);/* bind */ret = bind(sock_fd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr));if(-1 == ret)
    {fprintf(stderr,"bind error:%s
a", strerror(errno));
        close(sock_fd);exit(1);
    }/* listen */ret = listen(sock_fd, BACKLOG);if (-1 == ret)
    {fprintf(stderr,"listen error:%s
a", strerror(errno));
        close(sock_fd);exit(1);
    }/* accept */while(1)
    {
        addr_len = sizeof(struct sockaddr);
        new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &addr_len);if (-1 == new_fd)
        {fprintf(stderr,"accept error:%s
a", strerror(errno));
            close(sock_fd);exit(1);
        }

        client_num++;fprintf(stderr, "Server get connetion form client%d: %s
", client_num, inet_ntoa(client_addr.sin_addr));if (!fork())
        {/* Child process */while (1)
            {/* recv */recv_len = recv(new_fd, recv_buf, 999, 0);if (recv_len <= 0)
                {fprintf(stderr, "recv error:%s
a", strerror(errno));
                    close(new_fd);  exit(1);
                }else{
                    recv_buf[recv_len] = '�';printf("Get msg from client%d: %s
", client_num, recv_buf);
                }
            }   
            close(new_fd);          
        }      
    }/* close */close(sock_fd);exit(0); 
}
/*
* tcp_client.c
# Copyright (C) 2017 hceng, <huangcheng.job@foxmail.com>
# Licensed under terms of GPLv2
#
# This program is used for TCP / UDP learning.
# https://hceng.cn/
*/#include <stdlib.h>#include <stdio.h>#include <errno.h>#include <string.h>#include <netdb.h>#include <sys/types.h>#include <netinet/in.h>#include <sys/socket.h>#include <arpa/inet.h>#include <unistd.h>#define PORT_NUMBER 8888/* socket->connect->send->close*/int main(int argc, char *argv[])
{int sock_fd;struct sockaddr_in server_addr;int ret;unsigned char send_buf[1000];int send_len;if(argc != 2)
    {fprintf(stderr, "Usage:%s hostname
a", argv[0]);exit(1);
    }/* socket */sock_fd = socket(AF_INET, SOCK_STREAM, 0);//AF_INET:IPV4;SOCK_STREAM:TCPif (-1 == sock_fd)
    {fprintf(stderr,"socket error:%s
a", strerror(errno));exit(1);
    }/* set sockaddr_in parameter*/memset(&server_addr, 0, sizeof(struct sockaddr_in));//clearserver_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT_NUMBER);
    ret = inet_aton(argv[1], &server_addr.sin_addr);if(0 == ret)
    {fprintf(stderr,"server_ip error.
");
        close(sock_fd);exit(1);
    }/* connect */ret = connect(sock_fd, (const struct sockaddr *)&server_addr, sizeof(struct sockaddr)); if (-1 == ret)
    {fprintf(stderr,"connect error:%s
a", strerror(errno));
        close(sock_fd);exit(1);
    }while (1)
    {if (fgets(send_buf, 999, stdin))
        {/* send */send_len = send(sock_fd, send_buf, strlen(send_buf), 0);if (send_len <= 0)
            {fprintf(stderr,"send error:%s
a", strerror(errno));
                close(sock_fd);exit(1);
            }
        }
    }/* close */close(sock_fd);exit(0);
}

3.4 测试结果

先在Ubuntu主机上交叉编译服务器端代码,再在Ubuntu主机上编译客户端代码。
在开发板上运行服务器端代码,在Ubuntu主机先启动tmux分屏,再分别运行客户端代码。
- 服务器端

- 客户机端

4.UDP

UDP没有传递保证机制,如果传输中数据丢失,协议不会有任何的检测或提示。
这样的好处是传输的数据是持续的,此外它是无连接的传输,比如实时视频时,如果采用TCP,中途有一点点数据出错都会卡住,进行等待,产生延时。加入使用UDP,尽管有少量的丢帧,但数据是实时的。

4.1 UDP流程图

4.2 UDP步骤分析 ##

从流程图可以看出,UDP比TCP的步骤少多了。
- 服务器端:

a. 创建socket

    if (-1 == sock_fd)
    {
        fprintf(stderr,"socket error:%sa", strerror(errno));exit(1);
    }

协议族改成SOCK_DGRAM。

b. 设置socket

    server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY:This machine all IPserver_addr.sin_port = htons(PORT_NUMBER);

和前面的TCP设置还是一样的。

c. 绑定socket

    if(-1 == ret){fprintf(stderr,"bind error:%s
a", strerror(errno));close(sock_fd);exit(1);
    }

绑定的操作也没有变。

d. 接收数据

    recv_len = recvfrom(sock_fd, recv_buf, 999, 0, (struct sockaddr *)&server_addr, &addr_len);if (recv_len <= 0)
    {
        fprintf(stderr, "recvfrom error:%sa", strerror(errno));close(sock_fd); exit(1);
    }else{
        recv_buf[recv_len] = '�';printf("Get msg from client%d: %s", client_num, recv_buf);
    }

所需要头文件
#include <sys/types.h>
#include <sys/socket.h>
函数格式
int recvfrom(int sockfd, char FAR *buf, int len, int flags, struct sockaddr FAR *from, int FAR *fromlen);
函数功能
 从套接字上接收一个数据报并保存源地址;
 sockfd:标识一个已连接套接字的描述符
 buf:接收数据缓冲区
 len:接收数据缓冲区长度
 flags:调用操作方式,由以下零个或多个组成

flags说明recvsend
MSG_DONTROUTE绕过路由表查找
MSG_DONTWAIT仅本操作非阻塞
MSG_OOB发送或接收带外数据
MSG_PEEK窥看外来消息
MSG_WAITALL等待所有数据

from:(可选)指针,指向装有源地址的缓冲区
 fromlen:(可选)指针,指向from缓冲区长度值
返回值
 若成功,返回读入的字节数,否则返回0;

e. 关闭

    exit(0);
  • 客户机端:

a. 创建socket

    if (-1 == sock_fd)
    {
        fprintf(stderr,"socket error:%sa", strerror(errno));exit(1);
    }

协议族改成SOCK_DGRAM。

b. 设置socket

    server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT_NUMBER);ret = inet_aton(argv[1], &server_addr.sin_addr);if(0 == ret)
    {
        fprintf(stderr,"server_ip error.
");close(sock_fd);exit(1);}

c. 发送数据

    send_len = sendto(sock_fd, send_buf, strlen(send_buf), 0, (const struct sockaddr *)&server_addr, addr_len);if (send_len <= 0)
    {fprintf(stderr,"send error:%s
a", strerror(errno));
        close(sock_fd);exit(1);
    }

所需要头文件

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

函数格式
int sendto(int sockfd, char FAR *buf, int len, int flags, struct sockaddr FAR *to, int FAR *tolen);
函数功能
向一指定目的地发送数据;
sockfd:一个标识套接字的描述字
buf:发送数据缓冲区
len:发送数据缓冲区长度
flags:调用方式标志位
to:(可选)指针,指向目的的套接字的地址
tolen:目的套接字地址的长度
返回值
若成功,返回发送的字节数,如果连接已中止,返回0,如果发生错误,返回-1;

d. 关闭

close(sock_fd);exit(0);

UDP传输的客户端少了connect(),原本该在connect()函数里传入服务器地址相关信息,现在变成了在sendto()里传入。

4.3 UDP完整代码 ##

/*
* udp_server.c
# Copyright (C) 2017 hceng, <huangcheng.job@foxmail.com>
# Licensed under terms of GPLv2
#
# This program is used for TCP / UDP learning.
# https://hceng.cn/
*/#include <stdlib.h>#include <errno.h>#include <sys/types.h>      #include <sys/socket.h>#include <string.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <stdio.h>#include <signal.h>#define PORT_NUMBER 8888/* socket->bind->recvfrom/sendto->close */int main(int argc, char **argv)
{int sock_fd;struct sockaddr_in server_addr;struct sockaddr_in client_addr;int ret;int addr_len;int recv_len;unsigned char recv_buf[1000];/* socket */sock_fd = socket(AF_INET, SOCK_DGRAM, 0);//AF_INET:IPV4;SOCK_DGRAM:UDPif (-1 == sock_fd)
    {fprintf(stderr,"socket error:%s
a", strerror(errno));exit(1);
    }/* set sockaddr_in parameter*/memset(&server_addr, 0, sizeof(struct sockaddr_in));//clearserver_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY:This machine all IPserver_addr.sin_port = htons(PORT_NUMBER);/* bind */ret = bind(sock_fd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr));if(-1 == ret)
    {fprintf(stderr,"bind error:%s
a", strerror(errno));
        close(sock_fd);exit(1);
    }while (1)
    {/* recvfrom */addr_len = sizeof(struct sockaddr);
        recv_len = recvfrom(sock_fd, recv_buf, 999, 0, (struct sockaddr *)&client_addr, &addr_len);if (recv_len <= 0)
        {fprintf(stderr, "recvfrom error:%s
a", strerror(errno));
            close(sock_fd); exit(1);
        }else{
            recv_buf[recv_len] = '�';printf("Get msg from client:%s: %s
", inet_ntoa(client_addr.sin_addr), recv_buf);
        }
    }