【LwIP学习营】【第一周】网络通信基础及实现TCP 聊天客户端

发布于 2018-05-16 13:59:35
[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]


最终效果:


xuexi.png

查看更多

关注者
0
被浏览
1.7k
7 个回答
来一颗糖
来一颗糖 2018-05-16
    本帖最后由 来一颗糖 于 2018-5-17 16:01 编辑


学习的过程中 查阅的资料
下载附件[lwip学习笔记1.rar]
shenlan
shenlan 2018-05-16
来一颗糖 发表于 2018-5-16 14:02
学习的过程中 查阅的资料


糖糖果然无处不在
我夏了夏天
我夏了夏天 2018-05-16
这个编程用的是 POSIX 接口吧?
来一颗糖
来一颗糖 2018-05-16
Summer_gift 发表于 2018-5-16 19:59
这个编程用的是 POSIX 接口吧?


这些函数名称是sockets.h里封装后的.应该是吧,并不是很了解 POSIX 标准:o
armink
armink 2018-05-16
赞,逻辑清晰,文档整齐明了
来一颗糖
来一颗糖 2018-05-17
    本帖最后由 来一颗糖 于 2018-5-17 14:45 编辑


TCP客户端聊天程序


#include 

#include /* 使用BSD socket,需要包含socket.h头文件 */
#include "netdb.h"
#include "lwip/sockets.h"

#define BUFSZ 1024

static char *recv_data;
static int sock = -1, bytes_received;

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;
}

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 !\n");
}
}
MSH_CMD_EXPORT(connectchat, connectchat);

void login(int argc, char **argv)
{
if (sock != -1)
{
if (argc == 2)
{
send(sock, "login ", strlen("login "), 0);
send(sock, argv[1], strlen(argv[1]), 0);
send(sock, "\r\n", strlen("\r\n"), 0);
}
else
{
rt_kprintf("ERROR command !\n");
}
}
else
{
rt_kprintf("warning: Please use connectchat first !\n");
}
}
MSH_CMD_EXPORT(login, login xxx);

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!);

      void look(int argc, char **argv)
      {
      if (sock != -1)
      {
      if (argc == 1)
      {
      send(sock, "look ", strlen("look "), 0);
      send(sock, "\r\n", strlen("\r\n"), 0);
      }
      else
      {
      rt_kprintf("ERROR command !\n");
      }
      }
      else
      {
      rt_kprintf("warning: Please use connectchat first !\n");
      }
      }
      MSH_CMD_EXPORT(look, look);

      void who(int argc, char **argv)
      {
      if (sock != -1)
      {
      if (argc == 1)
      {
      send(sock, "who ", strlen("who "), 0);
      send(sock, "\r\n", strlen("\r\n"), 0);
      }
      else
      {
      rt_kprintf("ERROR command !\n");
      }
      }
      else
      {
      rt_kprintf("warning: Please use connectchat first !\n");
      }
      }
      MSH_CMD_EXPORT(who, who);

      void logout(int argc, char **argv)
      {
      if (sock != -1)
      {
      if (argc == 1)
      {
      send(sock, "logout ", strlen("logout "), 0);
      send(sock, "\r\n", strlen("\r\n"), 0);
      }
      else
      {
      rt_kprintf("ERROR command !\n");
      }
      }
      else
      {
      rt_kprintf("warning: Please use connectchat first !\n");
      }
      }
      MSH_CMD_EXPORT(logout, logout);

撰写答案

请登录后再发布答案,点击登录

发布
问题

分享
好友