物联网lwIP网络开发(Socket编程)

本文详细介绍了Socket的基本概念、通信原理、网络编程接口,包括lwIP实现的socket,以及预备知识如大端小端模式、地址转换接口、socket编程模型和常用函数如bind、listen、accept、connect、recv和send等。涵盖了TCP/IP协议族中的IPv4/6和Unix域套接字,以及UDP的使用情况。
摘要由CSDN通过智能技术生成

#学习记录

Socket基本概念

socket 基本概念

Socket(套接字)是计算机网络通信中的一个基本概念,它提供了一种在计算机网络中进行通信的方式。Socket可以看作是计算机网络通信中的一个端点,它允许两个或多个进程之间进行通信和数据交换。

socket通讯原理示意

sfd Server(服务器)端文件描述符

cfd Client(客户端)文件描述符

将socket抽象成文件进行操作(open、close、read、writ)

网络编程接口

lwIP怎么实现的socket

Socket编程预备知识1

大端模式和小端模式

11 22 33  44存储数据 如u32

内存存储方式:大端(高位字节放低地址如11放在位置0)、小端(低位字节放高地址,如44放在位置3)

网络字节序

起源

地址转换接口

htonl (host主机to net long)把主机u32的数据转换为网络的字节序数据,无论之前如何最后都会被转化为大端模式

ntohl网络字节序转换成本机字节序

//def.h
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

IP地址转换

socket 接口

pton 将IP地址转化为网络字节序的地址

af 网络模式(ipv4/6)

src 点分形式的ip地址(192.168.0.0)

dst 目标对uint32(当ipv6时是64(因为ipv6数据是占用6byte(48bit))) 的数据

//sockets.h
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
socklen_t目标(dst)源地址的长度,方便解析​

lwIP接口

仅针对ipv4(u32)的接口,下面函数功能大同小异

//inet.h
#define inet_addr(cp)                   ipaddr_addr(cp)
//将点分格式字符传传入cp后返回u32网络字节序对应ip地址的值

#define inet_aton(cp, addr)             ip4addr_aton(cp, (ip4_addr_t*)addr)
#define inet_ntoa(addr)                 ip4addr_ntoa((const ip4_addr_t*)&(addr))
#define inet_ntoa_r(addr, buf, buflen)  ip4addr_ntoa_r((const ip4_addr_t*)&(addr), buf, buflen)

Socket编程预备知识2

sockaddr数据结构

strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。

sockaddr

struct sockaddr {
  u8_t        sa_len;
  sa_family_t sa_family;   //ipv4/6
  char        sa_data[14];
};

sockaddr_in

struct sockaddr_in {
  u8_t            sin_len;       //结构体长度
  sa_family_t     sin_family;    //写AF_INET       (ipv4)
  in_port_t       sin_port;      //
  struct in_addr  sin_addr;      // 为了兼容性
#define SIN_ZERO_LEN 8           //填充
  char            sin_zero[SIN_ZERO_LEN];
};

sockaddr_in6

struct sockaddr_in6 {
  u8_t            sin6_len;      /* length of this structure    */
  sa_family_t     sin6_family;   /* AF_INET6                    */
  in_port_t       sin6_port;     /* Transport layer port #      */
  u32_t           sin6_flowinfo; /* IPv6 flow information       */
  struct in6_addr sin6_addr;     /* IPv6 address                */
  u32_t           sin6_scope_id; /* Set of interfaces for scope */
};

socklen_t

typedef u32_t socklen_t;   

socket 编程模型

Socket接口函数分析上

//sockets.h
#define accept(s,addr,addrlen)                    lwip_accept(s,addr,addrlen)
/** @ingroup socket */
#define bind(s,name,namelen)                      lwip_bind(s,name,namelen)
/** @ingroup socket */
#define shutdown(s,how)                           lwip_shutdown(s,how)
/** @ingroup socket */
#define getpeername(s,name,namelen)               lwip_getpeername(s,name,namelen)
/** @ingroup socket */
#define getsockname(s,name,namelen)               lwip_getsockname(s,name,namelen)
/** @ingroup socket */
#define setsockopt(s,level,optname,opval,optlen)  lwip_setsockopt(s,level,optname,opval,optlen)
/** @ingroup socket */
#define getsockopt(s,level,optname,opval,optlen)  lwip_getsockopt(s,level,optname,opval,optlen)
/** @ingroup socket */
#define closesocket(s)                            lwip_close(s)
/** @ingroup socket */
#define connect(s,name,namelen)                   lwip_connect(s,name,namelen)
/** @ingroup socket */
#define listen(s,backlog)                         lwip_listen(s,backlog)
/** @ingroup socket */
#define socket(domain,type,protocol)              lwip_socket(domain,type,protocol)
/** @ingroup socket */
#define select(maxfdp1,readset,writeset,exceptset,timeout)     lwip_select(maxfdp1,readset,writeset,exceptset,timeout)
/** @ingroup socket */
#define ioctlsocket(s,cmd,argp)                   lwip_ioctl(s,cmd,argp)

socket

int socket(int domain, int type, int protocol);
domain:
    AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
    AF_INET6 与上面类似,不过是来用IPv6的地址
    AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type:
    (重点)SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
   (重点)SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
    SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
    SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
    SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
protocol:
    传0 表示使用默认协议。(一般目前也就是默认传0)
返回值:
    成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
        

socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可

bind

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
    socket文件描述符
addr:
    构造出IP地址加端口号
addrlen:
    sizeof(addr)长度
返回值:
    成功返回0,失败返回-1, 设置errno
​

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。

bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
​

首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666

listen

backlog一般默认是5,在unix里是128

int listen(int sockfd, int backlog);
sockfd:
    socket文件描述符
backlog:
    排队建立3次握手队列和刚刚建立3次握手队列的链接数和

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
    socket文件描述符
addr:
    传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
    传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
    成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
​

三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。

connect

非阻塞,即无法连接时立刻返回-1,不会阻塞等待

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
    socket文件描述符
addr:
    传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
    传入参数,传入sizeof(addr)大小
返回值:
    成功返回0,失败返回-1,设置errno

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的(服务器)地址。connect()成功返回0,出错返回-1。

Socket接口函数分析下

/** @ingroup socket */
#define recv(s,mem,len,flags)                     lwip_recv(s,mem,len,flags)
/** @ingroup socket */
#define recvfrom(s,mem,len,flags,from,fromlen)    lwip_recvfrom(s,mem,len,flags,from,fromlen)
/** @ingroup socket */
#define send(s,dataptr,size,flags)                lwip_send(s,dataptr,size,flags)
/** @ingroup socket */
#define sendmsg(s,message,flags)                  lwip_sendmsg(s,message,flags)
/** @ingroup socket */
#define sendto(s,dataptr,size,flags,to,tolen)     lwip_sendto(s,dataptr,size,flags,to,tolen)

//#if LWIP_POSIX_SOCKETS_IO_NAMES
 文件相关操作 posix接口封装
/** @ingroup socket */
#define read(s,mem,len)                           lwip_read(s,mem,len)
/** @ingroup socket */
#define write(s,dataptr,len)                      lwip_write(s,dataptr,len)
/** @ingroup socket */
#define writev(s,iov,iovcnt)                      lwip_writev(s,iov,iovcnt)
/** @ingroup socket */
#define close(s)                                  lwip_close(s)
/** @ingroup socket */
#define fcntl(s,cmd,val)                          lwip_fcntl(s,cmd,val)
/** @ingroup socket */
#define ioctl(s,cmd,argp)                         lwip_ioctl(s,cmd,argp)

write && read

write

int write(int fd,const void *buf,size_t nbytes);
Write函数将buf中的nbytes字节内容写入到文件描述符中,成功返回写的字节数,
失败返回-1.并设置errno变量。在网络程序中,当我们向套接字文件描述舒服写
数据时有两种可能:
​
1、write的返回值大于0,表示写了部分数据或者是全部的数据,这样用一个while
循环不断的写入数据,但是循环过程中的buf参数和nbytes参数是我们自己来更新的,
也就是说,网络编程中写函数是不负责将全部数据写完之后再返回的,说不定中途就返回了!
​
2、返回值小于0,此时出错了,需要根据错误类型进行相应的处理。
​
如果错误是EINTR表示在写的时候出现了中断错误,如果是EPIPE表示网络连接出现了问题。

read

读到的数据存到buf,nbyte表示要读几个字节(nbyte ≠ read)

read只等于当前接收到的数据的大小是多少,而不是完成了多少nbyte数据的接收

int read(int fd,void *buf,size_t nbyte)
​
Read函数是负责从fd中读取内容,当读取成功时,read返回实际读取到的字节数,
如果返回值是0,表示已经读取到文件的结束了,小于0表示是读取错误。
​
如果错误是EINTR表示在写的时候出现了中断错误,如果是EPIPE表示网络连接出现了问题。

send && recv

(用于TCP接口的)

send

int send(SOCKET s, const char FAR *buf, int len, int flags);
功能:向TCP连接的另一端发送数据,客户程序一般用send函数向服务器发送请求,
而服务器则通常用send函数来向客户程序发送应答。
s:指定发送端套接字描述符;/文件描述符
buf: 指明一个存放应用程序要发送数据的缓冲区;
len: 指明实际要发送的数据的字节数;
flags: 一般置0。

只描述同步Socket的send函数的执行流程:
1、当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲的长度,
如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;
如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发
送缓冲中的数据,如果是就等待协议把数据发送完,
如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send
就比较s的发送缓冲区的剩余空间和len,
如果len大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发送完,如果
len小于剩余空间大小send就仅仅把buf中的数据copy到剩余空间里
(send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里,协议完成传输)。
2、
如果send函数copy数据成功,就返回实际copy的字节数,
如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;
如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。
​
注意:
1、send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是
此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,
那么下一个Socket函数就会返回SOCKET_ERROR。
2、每一个除send外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数
据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回SOCKET_ERROR
在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收
到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
 

recv

int recv(SOCKET s, char FAR *buf, int len, int flags);

功能:  从TCP连接的另一端接收数据,客户端、服务器端应用程序都用recv函数接收数据
s:    指定接收端套接字描述符;
buf:  指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
len:  指明buf的长度;
flags:一般置0。

只描述同步Socket的recv函数的执行流程:
1、当调用recv函数时,recv先等待s的发送缓冲中的数据被协议传送完毕,
如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,
如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收
缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,
直到协议把数据接收完毕。

2、当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中
(协议接收到的数据可能大于buf的长度,这时就要调用几次recv函数才能把s的
接收缓冲中的数据copy完。recv函数仅是copy出数据,真正的接收数据是协议完成),

3、recv函数返回其实际copy的字节数。
​
如果recv在copy时出错,那么它返回SOCKET_ERROR;
如果recv函数在等待协议接收数据时网络中断了,那么它返回0。

sendto && recvfrom

(用于UDP接口的)(UDP无连接,关闭不需要握手)

sendto

int sendto(int sockfd, const void *msg,int len, unsigned int
 flags, const struct sockaddr *to, int tolen);

to:     表示目地机的IP地址和端口号信息,
tolen:  常常被赋值为sizeof(struct sockaddr)。
sendto  函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。

recvfrom

int recvfrom(int sockfd,void *buf,int len,unsigned int flags,
struct sockaddr *from,int *fromlen);

from:      是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。
fromlen:    常置为sizeof (struct sockaddr)。

当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。
recvfrom()函数返回接收到的字节数或当出现错误时返回-1,并置相应的errno。

  • 28
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值