[md]# LWIP学习笔记 #
2018/5/16
## 预备知识 ##
### TCP与UDP区别 ###
**TCP:** (Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
**UDP:** (User Datagram Protocol 用户数据报协议)是OSI(Open System Interconnection开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
1. TCP提供的是面向连接的、可靠的数据流传输;UDP提供的是非面向连接的、不可靠的数据流传输。
2. TCP提供可靠的服务,通过TCP连接传送的数据,无差错、不丢失,不重复,按序到达;UDP尽大努力交付,即不保证可靠交付。
3. TCP面向字节流;UDP面向报文。
4. TCP连接只能是点到点的;UDP支持一对一、一对多、多对一和多对多的交互通信。
5. TCP首部开销20字节;UDP的首部开销小,只有8个字节。
6. TCP的逻辑通信信道是全双工的可靠信道;UDP的逻辑通信信道是不可靠信道。
### TCP编程的服务器端一般步骤 ###
1. 创建一个socket,用函数socket();
2. 设置socket属性,用函数setsockopt(); * 可选
3. 绑定IP地址、端口等信息到socket上,用函数bind();
4. 开启监听,用函数listen();
5. 接收客户端上来的连接,用函数accept();
6. 收发数据,用函数send()和recv(),或者read()和write();
7. 关闭网络连接;
8. 关闭监听;
### TCP编程的客户端一般步骤 ###
1. 创建一个socket,用函数socket();
2. 设置socket属性,用函数setsockopt();* 可选
3. 绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4. 设置要连接的对方的IP地址和端口等属性;
5. 连接服务器,用函数connect();
6. 收发数据,用函数send()和recv(),或者read()和write();
7. 关闭网络连接;
### UDP编程的服务器端一般步骤 ###
1. 创建一个socket,用函数socket();
2. 设置socket属性,用函数setsockopt();* 可选
3. 绑定IP地址、端口等信息到socket上,用函数bind();
4. 循环接收数据,用函数recvfrom();
5. 关闭网络连接;
### UDP编程的客户端一般步骤 ###
1. 创建一个socket,用函数socket();
2. 设置socket属性,用函数setsockopt();* 可选
3. 绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4. 设置对方的IP地址和端口等属性;
5. 发送数据,用函数sendto();
6. 关闭网络连接;
### API函数 ###
----------
为通信创造一个端点并返回一个文件描述符。
```
int socket(int domain, int type, int protocol);
```
* 入口参数:
|参数 | 描述 |
---------------|--------------------------------
|domain|确定协议族;|
|type | 数据类型;|
|protocol | 协议;|
----------
----------
给套接字分配一个地址。当使用 socket()创造一个套接字时, 只是给定了协议族,并没有分配地址。在套接字能够接受来自其他主机的连接前,必须用bind()给它绑定一个地址。
```
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
```
* 入口参数:
|参数 | 描述 |
---------------|--------------------------------
|sockfd|代表socket的文件描述符。|
|my_addr |指向 sockaddr 结构体的指针,代表要绑定的地址 ;|
|addrlen | 是sockaddr结构体的大小。|
----------
----------
一旦一个套接字和一个地址联系之后,listen() 监听到来的连接。但是这只适用于对面向连接的模式,例如 套接字类型是 (SOCK_STREAM, SOCK_SEQPACKET)。
```
int listen(int sockfd, int backlog);
```
* 入口参数:
|参数 | 描述 |
---------------|--------------------------------
|sockfd|代表socket的文件描述符。|
|backlog |一个整数,表示一次能够等待的最大连接数目。操作系统通常会对这个值设置上限。|
----------
----------
当应用程序监听来自其他主机的面对数据流的连接时,通过事件(比如Unix select()系统调用)通知它。必须用 accept()函数初始化连接。 Accept() 为每个连接创立新的套接字并从监听队列中移除这个连接。
```
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
```
* 入口参数:
|参数 | 描述 |
---------------|--------------------------------
|sockfd|代表socket的文件描述符。|
|cliaddr | 指向sockaddr 结构体的指针,客户机地址信息。|
|addrlen | 指向 socklen_t的指针,确定客户机地址结构体的大小 。|
----------
----------
connect()系统调用为一个套接字设置连接,参数有文件描述符和主机地址。
```
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
```
* 入口参数:
|参数 | 描述 |
---------------|--------------------------------
|sockfd|监听的套接字描述符|
|serv_addr | 指向sockaddr 结构体的指针,服务器地址信息。|
|addrlen | sockaddr结构体的大小。|
----------
----------
gethostbyname() 和 gethostbyaddr()函数是用来解析主机名和地址的。可能会使用DNS服务或者本地主机上的其他解析机制(例如查询/etc/hosts)。返回一个指向 struct hostent的指针,这个结构体描述一个IP主机。
```
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, int len, int type);
```
----------
## 实现一个简单的 TCP 聊天客户端 ##
----------
在RT-Thread的MSH下connectchat命令将会创建一个线程.这个线程里,主要调用了tcpclient("112.124.34.90",5005)函数.tcpclinent()会创建一个TCP客户端,将接收到的信息打印出来.如果这个TCP客户端没有成功创建的话,那么是无法使用其他的聊天功能的.
```
void tcp_chat_thread(void *parameter)
{
rt_kprintf("\nplease waiting connect.\n");
tcpclient("112.124.34.90",5005);
rt_kprintf("\ndisconnect.\n");
}
int chat_init()
{
rt_thread_t tid;
/* 创建test线程 */
tid = rt_thread_create("chat",
tcp_chat_thread,
RT_NULL,
1024,
10,
20);
/* 创建成功则启动线程 */
if (tid != RT_NULL)
rt_thread_startup(tid);
return 0;
}
void connectchat(int argc, char **argv)
{
if(argc == 1)
{
chat_init();
}
else
{
rt_kprintf("ERROR command 3 !\n");
}
}
MSH_CMD_EXPORT(connectchat, connectchat);
```
----------
**tcpclient("112.124.34.90",5005);进行如下操作:**
1. 创建一个socket,socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
2. 设置要连接的对方的IP地址和端口等属性;
3. 连接服务器,connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr));
4. 接收数据,recv(sock, recv_data, BUFSZ - 1, 0);
5. 以上任意一步出错,则closesocket(sock);sock = -1;
(可以参考前面内容 ***TCP编程的客户端一般步骤*** )
```
void tcpclient(const char *url, int port)
{
struct sockaddr_in server_addr;
/* 分配用于存放接收数据的缓冲 */
recv_data = rt_malloc(BUFSZ);
if (recv_data == RT_NULL)
{
rt_kprintf("No memory\n");
return;
}
/* 创建一个socket,类型是SOCKET_STREAM,TCP类型 */
if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
{
/* 创建socket失败 */
rt_kprintf("Socket error\n");
/* 释放接收缓冲 */
rt_free(recv_data);
return;
}
/* 初始化预连接的服务端地址 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
ip4addr_aton(url, (ip4_addr_t *)&server_addr.sin_addr);
rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
/* 连接到服务端 */
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
{
/* 连接失败 */
rt_kprintf("\n Connect fail!\n");
closesocket(sock);
sock = -1;
/*释放接收缓冲 */
rt_free(recv_data);
return;
}
while (1)
{
/* 从sock连接中接收最大BUFSZ - 1字节数据 */
bytes_received = recv(sock, recv_data, BUFSZ - 1, 0);
/* 如果返回的<=0这说明出错,此时需要断开连接. */
if (bytes_received <= 0)
{
/* 接收失败,关闭这个连接 */
closesocket(sock);
sock = -1;
rt_kprintf("\nclose the socket.\r\n");
/* 释放接收缓冲 */
rt_free(recv_data);
break;
}
/* 有接收到数据,把末端清零 */
recv_data[bytes_received] = '\0';
/* 在控制终端显示收到的数据 */
rt_kprintf("\n%s ", recv_data);
}
return;
}
```
----------
(其它的命令原理都一样,就拿这个做例子.)
在msh下使用这个命令的时候,这个函数首先会判断sock不为-1,然后判断命令参数的数量是否符合要求,由于msh下是以空格分隔各个命令参数的,所以需要人为的给各个参数之间加上send(sock, " ", strlen(" "), 0);**(额,经学长提醒,这个操作如果在输入的参数空格太多的情况下会带来一个问题,就是如果say xxxxx 如果发送的消息空格太多的话,会造成一部分xxxxx发送不全.调大RT_FINSH_ARG_MAX的值算是一种不优雅的方法.应该有更合理的方法解决这个问题.)**
```
void say(int argc, char **argv)
{
if (sock != -1)
{
if (argc > 1)
{
send(sock, "say ", strlen("say "), 0);
for (rt_uint32_t i = 1; i <= argc; i++)
{
send(sock, " ", strlen(" "), 0);
send(sock, argv
, strlen(argv), 0);
}
send(sock, "\r\n", strlen("\r\n"), 0);
}
else
{
rt_kprintf("warning !\n");
}
}
else
{
rt_kprintf("warning: Please use connectchat first !\n");
}
}
MSH_CMD_EXPORT(say, say hello!);
```
[/md]
最终效果:
查看更多