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

2019-07-12 18:34发布

class="markdown_views prism-atom-one-light">

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 #include #include 函数格式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 #include #include 函数格式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 函数格式unsigned short int htons(unsigned short int hostshort) 函数功能
将参数指定的16位主机(host)字符顺序转换成网络(net)字符顺序;
hostshort:待转换的16位主机字符顺序数
返回值
返回对应的网络字符顺序数;
  • htonl()函数:
    所需要头文件:
#include 函数格式unsigned long int htons(unsigned long int hostlong) 函数功能
将参数指定的32位主机(host)字符顺序转换成网络(net)字符顺序;
hostlong:待转换的32位主机字符顺序数
返回值
返回对应的网络字符顺序数;
网络转主机:
  • ntohs()函数:
    所需要头文件:
#include 函数格式unsigned short int ntohs(unsigned short int netshort) 函数功能
将参数指定的16位网络(net)字符顺序转换成主机(host)字符顺序;
netshort:待转换的16位网络字符顺序数 返回值
返回对应的主机字符顺序数;
  • ntohl()函数:
    所需要头文件:
#include 函数格式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:%s a", strerror(errno)); exit(1); } 所需要头文件: #include #include 函数格式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 memset(&server_addr, 0, sizeof(struct sockaddr_in));//clear server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY:This machine all IP server_addr.sin_port = htons(PORT_NUMBER); 设置何种协议族,设置本机IP和端口,也就有了唯一性。 c. 绑定socket 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); } 所需要头文件: #include #include 函数格式int bind(int sockfd, struct sockaddr *addr, int addrlen); 函数功能
把套接字绑定到本地计算机的某一个端口上;
sockfd:待绑定的套接字描述符
addr:一个struct sockaddr *指针,指定要绑定给sockfd的协议地址。内容结构由前面的协议族决定。
addrlen:地址的长度 返回值
若成功,返回0,否则返回-1,错误信息存在errno中; d. 开始监听 ret = listen(sock_fd, BACKLOG); if (-1 == ret) { fprintf(stderr,"listen error:%s a", strerror(errno)); close(sock_fd); exit(1); } 所需要头文件: #include #include 函数格式int listen(int sockfd, int backlog); 函数功能
使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求,最大连接数量为backlog≤128;
sockfd:待监听的套接字描述符
backlog:最大可监听和连接的客户端数量 返回值
若成功,返回0,否则返回-1; e. 阻塞,等待连接 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); } 所需要头文件: #include #include 函数格式int accept(int sockfd, struct sockaddr *addr, int *addrlen); 函数功能
接受连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求;
当accept函数接受一个连接时,会返回一个新的socket标识符,以后的数据传输和读取就要通过这个新的socket编号来处理,原来参数中的socket也可以继续使用,继续监听其它客户机的连接请求; accept连接成功时,参数addr所指的结构体会填入所连接机器的地址数据;
sockfd:待监听的套接字描述符
addr:指向struct sockaddr的指针,用于返回客户端的协议地址
addrlen:协议地址的长度 返回值
若成功,返回一个由内核自动生成的一个全新描述字,代表与返回客户的TCP连接,否则返回-1,错误信息存在errno中; f. 接收数据 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] = '