TCP(Transmission Control Protocol 传输控制协议):是一种面向连接、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
UDP(User Datagram Protocol 用户数据报协议):是OSI(Open System Interconnection 开放式系统互联):参考模型中的一种无连接的传输层协议,提供面向事务的简单不可靠传送服务。
OSI七层模型和TCP/IP四层模型详解请看此处
区别:
socket():
创建一个socketsetsockopt():
设置socket属性bind():
绑定IP地址、端口等信息到socket类上listen():
开启监听accept():
接收来自客户端的连接send()、recv()、read()、write()
socket():
创建一个socketsetsockopt():
设置socket属性,可选bind():
绑定IP地址、端口等信息到socket上recvfrom():
循环接收数据socket():
创建一个socketsetsockopt():
设置socket属性,可选bind():
绑定IP地址、端口等信息到socket上sendto():
发送数据SAL(套接字抽象层)是RT-Thread官方为避免系统对单一网络协议栈的依赖,同时也为适配更多网络协议栈类型而提供的一套网络组件,该组件主要完成对不同网络协议栈或网络实现接口的抽象并对上层一共一组标准BSD Socket API,这样开发者只需关心和使用网络应用层提供的网络接口,而无需关心底层具体网络协议栈类型和实现,极大提高了系统的兼容性。
SAL组件工作原理的介绍主要分为如下两部分:
由于不同协议栈或网络功能的实现,其网络接口的名称各有不同,已连接函数为例,lwip协议栈中接口名称为lwip_connect,而AT Socket网络实现接口为at_connect。通过SAL组件可以完成对不同协议栈或网络实现接口的抽象和统一,组件再socket创建时通过判断传入的协议簇(domain)类型来判断使用的协议栈或网络功能。
目前RT-Thread SAL组件支持的协议栈或网络实现类型有:LWIP协议栈(AT_INET)、AT Socket协议栈(AF_AT)、WIZnet硬件 TCP/IP协议栈(AT_WIZ)2。
int socket(int domain, int type, int protocol);
为了动态适配不同协议栈或网络实现的接入,SAL组件中对于每个协议栈或者网络实现提供两种协议类型匹配方式:主协议簇类型和次协议簇类型,在socket创建之初收i西安判断传入协议簇类型是否存在已经支持的主协议类型,如果是则使用对应协议栈或网络实现,如果不是则判断次协议簇类型是否支持。
具体而言,主协议簇类型是指一个协议簇的最基本类型,例如 IPv4 或 IPv6。次协议簇类型则是在主协议簇类型的基础上进行扩展或增强,例如 TCP 或 UDP 协议。主协议簇类型可以被多个次协议簇类型所支持,但一个次协议簇类型只能属于一个主协议簇类型。
目前系统支持协议簇类型如下:
LWIP协议栈:family = AF_INET、sec_family = AF_INET
AT Socket协议栈:family = AF_AT、sec_family = AF_INET
WIZnet硬件 TCP/IP协议栈:family = AF_WIZ
SAL组件的主要作用是统一BSD Socket API接口,我们以官方示例对SAL组件函数进行调用方式的实现:
/* SAL 组件为应用层提供的标准 BSD Socket API */
int connect(int s, const struct sockaddr *name, socklen_t namelen)
{
/* 获取 SAL 套接字描述符 */
int socket = dfs_net_getsocket(s);
/* 通过 SAL 套接字描述符执行 sal_connect 函数 */
return sal_connect(socket, name, namelen);
}
/* SAL 组件抽象函数接口实现 */
int sal_connect(int socket, const struct sockaddr *name, socklen_t namelen)
{
struct sal_socket *sock;
struct sal_proto_family *pf;
int ret;
/* 检查 SAL socket 结构体是否正常 */
SAL_SOCKET_OBJ_GET(sock, socket);
/* 检查当前 socket 网络连接状态是否正常 */
SAL_NETDEV_IS_COMMONICABLE(sock->netdev);
/* 检查当前 socket 对应的底层 operation 函数是否正常 */
SAL_NETDEV_SOCKETOPS_VALID(sock->netdev, pf, connect);
/* 执行底层注册的 connect operation 函数 */
ret = pf->skt_ops->connect((int) sock->user_data, name, namelen);
#ifdef SAL_USING_TLS
if (ret >= 0 && SAL_SOCKOPS_PROTO_TLS_VALID(sock, connect))
{
if (proto_tls->ops->connect(sock->user_data_tls) < 0)
{
return -1;
}
return ret;
}
#endif
return ret;
}
/* lwIP 协议栈函数底层 connect 函数实现 */
int lwip_connect(int socket, const struct sockaddr *name, socklen_t namelen)
{
...
}
在TCP、UDP等协议数据传输时,由于数据包是明文的,所以很可能被拦截,甚至被解析出数据,为了保证网络传输的安全性,需要用户在应用层和传输层之间添加SSL/TLS协议。
TLS(Transport Layer Security,传输层安全协议)是建立在传输层TCP协议之上的协议,其前身是SSL(Secure Socket Layer,安全套接字层),主要作用是将应用层的报文进行非对称加密后再由TCP协议进行传输,实现了数据的加密安全交互。3
对于通过的加密方式,需要使用其指定的加密接口和流程进行加密,而SAL TLS功能的主要作用是提供Socket层面的TLS加密传输特性,抽象多种TLS处理方式,提供统一的接口用于完成TLS数据交互。
使用流程:
配置完成后,需要在socket创建时传入的potocol
类型是使用PROTOCOL_TLS或者PROTOCOL_DTLS,即可使用标准BSD Socket API接口,完成TLS连接的建立和数据的收发。
示例如下,参考RT-Threda文档中心:
#include
#include
#include
#include
#include
/* RT-Thread 官网,支持 TLS 功能 */
#define SAL_TLS_HOST "www.rt-thread.org"
#define SAL_TLS_PORT 443
#define SAL_TLS_BUFSZ 1024
static const char *send_data = "GET /download/rt-thread.txt HTTP/1.1\r\n"
"Host: www.rt-thread.org\r\n"
"User-Agent: rtthread/4.0.1 rtt\r\n\r\n";
void sal_tls_test(void)
{
int ret, i;
char *recv_data;
struct hostent *host;
int sock = -1, bytes_received;
struct sockaddr_in server_addr;
/* 通过函数入口参数url获得host地址(如果是域名,会做域名解析) */
host = gethostbyname(SAL_TLS_HOST);
recv_data = rt_calloc(1, SAL_TLS_BUFSZ);
if (recv_data == RT_NULL)
{
rt_kprintf("No memory\n");
return;
}
/* 创建一个socket,类型是SOCKET_STREAM,TCP 协议, TLS 类型 */
if ((sock = socket(AF_INET, SOCK_STREAM, PROTOCOL_TLS)) < 0)
{
rt_kprintf("Socket error\n");
goto __exit;
}
/* 初始化预连接的服务端地址 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SAL_TLS_PORT);
server_addr.sin_addr = *((struct in_addr *)host->h_addr);
rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
{
rt_kprintf("Connect fail!\n");
goto __exit;
}
/* 发送数据到 socket 连接 */
ret = send(sock, send_data, strlen(send_data), 0);
if (ret <= 0)
{
rt_kprintf("send error,close the socket.\n");
goto __exit;
}
/* 接收并打印响应的数据,使用加密数据传输 */
bytes_received = recv(sock, recv_data, SAL_TLS_BUFSZ - 1, 0);
if (bytes_received <= 0)
{
rt_kprintf("received error,close the socket.\n");
goto __exit;
}
rt_kprintf("recv data:\n");
for (i = 0; i < bytes_received; i++)
{
rt_kprintf("%c", recv_data[i]);
}
__exit:
if (recv_data)
rt_free(recv_data);
if (sock >= 0)
closesocket(sock);
}
#ifdef FINSH_USING_MSH
#include
MSH_CMD_EXPORT(sal_tls_test, SAL TLS function test);
#endif /* FINSH_USING_MSH */
为通信创建一个端点并返回一个文件描述符
int socket(int domain, int type, int protocol);
# domain / 协议族类型
AF_INET # IPv4 协议族
AF_INET6 # IPv6 协议族
# type / 协议类型
/* Socket protocol types (1:TCP/2:UDP/3:RAW) */
#define SOCK_STREAM 1
#define SOCK_DGRAM 2
#define SOCK_RAW 3
当使用socket()创造一个套接字时,只是给定了协议簇,并没有分配地址。在套接字能够接收来自其他主机的连接时,必须bind()给它绑定一个地址。
int bind(int s, const struct sockaddr *name, socklen_t namelen);
附:SAL组件依赖netdev组件,当使用bind()函数时,可通过netdev网卡名称获取网卡对象中IP地址信息,用于将创建的Socket套接字绑定到指定的网卡对象。
来自RT-Thread文档中心,完成通过传入的网卡名称绑定该网卡IP地址并和服务器进行连接的过程:
#include
#include
#include
#define SERVER_HOST "192.168.1.123"
#define SERVER_PORT 1234
static int bing_test(int argc, char **argv)
{
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
struct netdev *netdev = RT_NULL;
int sockfd = -1;
if (argc != 2)
{
rt_kprintf("bind_test [netdev_name] --bind network interface device by name.\n");
return -RT_ERROR;
}
/* 通过名称获取 netdev 网卡对象 */
netdev = netdev_get_by_name(argv[1]);
if (netdev == RT_NULL)
{
rt_kprintf("get network interface device(%s) failed.\n", argv[1]);
return -RT_ERROR;
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
rt_kprintf("Socket create failed.\n");
return -RT_ERROR;
}
/* 初始化需要绑定的客户端地址 */
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(8080);
/* 获取网卡对象中 IP 地址信息 */
client_addr.sin_addr.s_addr = netdev->ip_addr.addr;
rt_memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));
if (bind(sockfd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr)) < 0)
{
rt_kprintf("socket bind failed.\n");
closesocket(sockfd);
return -RT_ERROR;
}
rt_kprintf("socket bind network interface device(%s) success!\n", netdev->name);
/* 初始化预连接的服务端地址 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_HOST);
rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
/* 连接到服务端 */
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
{
rt_kprintf("socket connect failed!\n");
closesocket(sockfd);
return -RT_ERROR;
}
else
{
rt_kprintf("socket connect success!\n");
}
/* 关闭连接 */
closesocket(sockfd);
return RT_EOK;
}
#ifdef FINSH_USING_MSH
#include
MSH_CMD_EXPORT(bing_test, bind network interface device test);
#endif /* FINSH_USING_MSH */
当有一个套接字和一个地址联系之后,listen()监听到来的连接。只适用于面向连接的模式。
int listen(int s, int backlog);
当应用程序监听来自其他他主机的面向数据流的连接时,通过事件通知它,必须用accept()函数初始化连接。该函数为每个连接创建新的套接字并从监听队列中移除这个连接。
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
该函数用于建立与指定 socket 的连接。
int connect(int s, const struct sockaddr *name, socklen_t namelen);
该函数常用于 TCP 连接发送数据。
int send(int s, const void *dataptr, size_t size, int flags);
该函数用于TCP连接接收数据。
int recv(int s, void *mem, size_t len, int flags);
该函数用于UDP连接发送数据。
int sendto(int s, const void *dataptr, size_t size, int flags, const struct sockaddr *to, socklen_t tolen);
该函数用于UDP连接发送数据。
int recvfrom(int s, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
网络协议栈或网络功能实现的接入,主要是对协议簇结构体的初始化和注册处理,并且添加到SAL组件中协议簇列表中,协议簇结构体定义如下:
/* network interface socket opreations */
struct sal_socket_ops
{
int (*socket) (int domain, int type, int protocol);
int (*closesocket)(int s);
int (*bind) (int s, const struct sockaddr *name, socklen_t namelen);
int (*listen) (int s, int backlog);
int (*connect) (int s, const struct sockaddr *name, socklen_t namelen);
int (*accept) (int s, struct sockaddr *addr, socklen_t *addrlen);
int (*sendto) (int s, const void *data, size_t size, int flags, const struct sockaddr *to, socklen_t tolen);
int (*recvfrom) (int s, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
int (*getsockopt) (int s, int level, int optname, void *optval, socklen_t *optlen);
int (*setsockopt) (int s, int level, int optname, const void *optval, socklen_t optlen);
int (*shutdown) (int s, int how);
int (*getpeername)(int s, struct sockaddr *name, socklen_t *namelen);
int (*getsockname)(int s, struct sockaddr *name, socklen_t *namelen);
int (*ioctlsocket)(int s, long cmd, void *arg);
#ifdef SAL_USING_POSIX
int (*poll) (struct dfs_fd *file, struct rt_pollreq *req);
#endif
};
/* sal network database name resolving */
struct sal_netdb_ops
{
struct hostent* (*gethostbyname) (const char *name);
int (*gethostbyname_r)(const char *name, struct hostent *ret, char *buf, size_t buflen, struct hostent **result, int *h_errnop);
int (*getaddrinfo) (const char *nodename, const char *servname, const struct addrinfo *hints, struct addrinfo **res);
void (*freeaddrinfo) (struct addrinfo *ai);
};
/* 协议簇结构体定义 */
struct sal_proto_family
{
int family; /* primary protocol families type */
int sec_family; /* secondary protocol families type */
const struct sal_socket_ops *skt_ops; /* socket opreations */
const struct sal_netdb_ops *netdb_ops; /* network database opreations */
};
伯克利套接字(Berkeley sockets),也称BSD Socket,伯克利套接字的应用编程接口(API)是采用C语言的进程间通信的库,经常用在计算机网络间的通信。 BSD Socket的应用编程接口已经是网络套接字的抽象标准。大多数其他程序语言使用一种相似的编程接口。最初是由加州伯克利大学为Unix系统开发出来。 ↩︎
WIZnet的硬件TCP/IP协议栈采用了TOE(TCP/IP Core Offload Engine)技术,将TCP/IP协议栈等网络处理功能转移到专用硬件中,从而减少了CPU的负担,提高了整个系统的性能和稳定性。同时,WIZnet的硬件TCP/IP协议栈还支持多种网络协议,并提供了Socket API封装等高层次接口,方便用户进行开发和集成。 ↩︎
在 TLS 协议中,使用了非对称加密和对称加密两种加密方式。其中,非对称加密主要用于密钥协商和身份认证,而对称加密则用于数据传输的加密和解密。在TLS握手过程中,客户端和服务器会相互发送自己的公钥,并通过对方的公钥加密生成一个随机数的方式协商出用来进行对称加密的对称密钥。这个对称密钥就是用非对称加密算法加密后的数据包。接收方拿到这个数据包后,使用自己的私钥进行解密,获取生成的对称密钥。然后,双方就开始使用协商好的对称密钥进行数据传输。接收方会利用对称密钥对收到的数据进行解密,得到明文数据。这样,在整个数据传输过程中,只有公钥被公开,密钥等关键信息都是使用非对称加密算法进行加密传输的,保证了安全性。总之,在 TLS 协议中,接收方通过使用自己的私钥解密协商出的对称密钥,从而完成对加密数据的解析。这个过程是整个 TLS 协议中非常重要的一个环节,确保了加密数据在传输过程中的安全性和可靠性。 ↩︎