嵌入式 Linux网络编程(一)——Socket网络编程基础
2019-07-12 14:28 发布
生成海报
嵌入式 Linux 网络编程一—— Socket 网络编程基础 一、 Socket简介 1、 网络中进程间通信 本机进程使用进程号区别不同的进程进程间通信方式有管道、信号、消息队列、共享内存、信号量等。网络中进程间的通信首先需要识别进程所在主机在网络中的唯一标识即网络层的IP 地址主机上的进程可以通过传输层的协议与端口号识别。 2、 Socket 原理 Socket是应用层与TCP/IP协议族通信的中间软件抽象层是一种编程接口。Socket屏蔽了不同网络协议的差异支持面向连接(Transmission Control Protocol - TCPIP)和无连接(User Datagram Protocol-UDP 和 Inter-Network Packet Exchange-IPX)的传输协议。 二、Socket 通信的基础知识 1、网络字节序 主机字节序即内存中存储字节的方式分为大端序和小端序。何为大端、小端呢小端将低字节存储在低地址。大端将高字节存储在低字节。网络中在处理多字节顺序时一般采用大端序。在网络传输时需要把主机字节序转换到网络字节序常用的转换函数如下 #include uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); 2、 数据结构 struct sockaddr { sa_family_t sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */ }; struct sockaddr_in { __kernel_sa_family_t sin_family; /* Address family */ __be16 sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ /* Pad to size of `struct sockaddr'. */ unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)]; }; /* Internet address. */ struct in_addr { __be32 s_addr; }; 3、 IP地址转换 int inet_aton(const char *cp, struct in_addr *inp); 将cp所指的字符串IP地址转换成32位的网络字节序IP地址 in_addr_t inet_addr(const char *cp); 将cp所指的字符串IP地址转换成32位的网络字节序IP地址返回 char *inet_ntoa(struct in_addr in); 将32位网络字节序IP地址转换成点分十进制的字符串IP地址 4、地址结构使用 A、 定义一个struct sockaddr_in类型的变量并清空 struct sockaddr_in server addr; bzero (& server addr, sizeof(server addr )); B、 填充地址信息 server addr.sin_family = A F_INET; server addr.sin_port = htons( 8080 ); server addr.sin_addr.s_addr = inet_addr("192.168. 6 .100"); C、 将该变量强制转换为struct sockaddr类型在函数中使用 bind(listenfd, (struct sockaddr *)& server addr, sizeof( server addr)); 5、 TCP 连接的建立 TCP协议通过三个报文段完成连接的建立 建立连接的过程称为三次握手(three-way handshake) 。 第一次握手建立连接时客户端发送syn包(syn=j)到服务器并进入SYN_SEND状态等待服务器确认SYN同步序列编号(Synchronize Sequence Numbers)。 第二次握手服务器收到syn包必须确认客户的SYNack=j+1同时自己也发送一个SYN包syn=k即SYN+ACK包此时服务器进入SYN_RECV状态 第三次握手客户端收到服务器的SYN+ACK包 向服务器发送确认包 ACK(ack=k+1) 此包发送完毕客户端和服务器进入 ESTABLISHED 状态完成三次握手。 一个完整的三次握手是 请求---应答---再次确认。 当客户端调用 connect 时触发了连接请求向服务器发送了 SYN J 包这时 connect 进入阻塞状态服务器监听到连接请求即收到 SYN J 包调用 accept 函数接收请求向客户端发送 SYN K ACK J+1 这时 accept 进入阻塞状态客户端收到服务器的 SYN K ACK J+1 之后这时 connect 返回并对 SYN K 进行确认服务器收到 ACK K+1 时 accept 返回至此三次握手完毕连接建立。 6、 TCP 连接的断开 终止一个连接要经过四次握手简称四次握手释放 TCP连接是全双工的每个方向都必须单独进行关闭。当一方完成数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭而另一方执行被动关闭。 A、客户端A发送一个FIN用来关闭客户A到服务器B的数据传送 B、服务器B收到这个FIN它发回一个ACK确认序号为收到的序号加1。和SYN一样一个FIN将占用一个序号。 C、服务器B关闭与客户端A的连接发送一个FIN给客户端A。 D、客户端A发回ACK报文确认并将确认序号设置为收到序号加1。 为什么建立连接协议是三次握手而关闭连接却是四次握手呢 因为服务端的 LISTEN 状态下的 SOCKET 当收到 SYN 报文的建连请求后它可以把 ACK 和 SYN ACK 起应答作用而 SYN 起同步作用放在一个报文里来发送。但关闭连接时当收到对方的 FIN 报文通知时仅仅表示对方没有数据发送给你了但你所有的数据未必都全部发送给对方了你未必会马上会关闭 SOCKET 你可能还需要发送一些数据给对方之后再发送 FIN 报文给对方来表示你同意现在可以关闭连接了所以 ACK 报文和 FIN 报文多数情况下都是分开发送的。 为什么 TIME_WAIT 状态还需要等 2MSL 后才能返回到 CLOSED 状态 虽然双方都同意关闭连接了而且握手的 4 个报文也都协调和发送完毕按理可以直接回到 CLOSED 状态就好比从 SYN_SEND 状态到 ESTABLISH 状态那样但是因为我们必须要假想网络是不可靠的你无法保证你最后发送的 ACK 报文会一定被对方收到因此对方处于 LAST_ACK 状态下的 SOCKET 可能会因为超时未收到 ACK 报文而重发 FIN 报文所以这个 TIME_WAIT 状态的作用就是用来重发可能丢失的 ACK 报文。 7、 getaddrinfo int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); node :一个主机名 域名 或者地址串 (IPv4 点分十进制串或者 IPv6 的 16 进制串 ) service服务名可以是十进制的端口号可以是已定义服务名如 ftp 、 http 等 hints可以是一个空指针也可以是一个指向某个 addrinfo结构体 的指针调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说如果指定的服务既支持 TCP 也支持 UDP 那么调用者可以把 hints 结构中的 ai_socktype 成员设置成 SOCK_DGRAM 使得返回的仅仅是适用于数据报套接口的信息。 result本函数通过 result 指针参数返回一个指向 addrinfo 结构体链表的指针。 返回值 0—— 成功非 0—— 出错 struct addrinfo { int ai_flags; int ai_family;// AF_INET,AF_INET6或者AF_UNSPEC int ai_socktype;// SOCK_STREAM or SOCK_DGRAM int ai_protocol;//0 size_t ai_addrlen; struct sockaddr *ai_addr; char *ai_canonname; struct addrinfo *ai_next; }; ai_flags: AI_PASSIVE套接字地址用于监听绑定 AI_CANONNAME需要一个规范名而不是别名 AI_V4MAPPED如果没有找到IPV6 地址返回映射到 IPV6 格式的 IPV4 地址 AI_ADDRCONFIG查询配置的地址类型IPV4 或 IPV6 AI_NUMERICSERV以端口号返回服务 AI_NUMERICHOST以数字格式返回主机地址 gethostbyname函数仅支持 IPV4 struct hostent *gethostbyname(const char *name); struct hostent { char *h_name; /* official name of host */ char **h_aliases; /* alias list */ int h_addrtype; /* host address type */ int h_length; /* length of address */ char **h_addr_list; /* list of addresses */ } #define h_addr h_addr_list[0] /* for backward compatibility */ name主机名或域名 三、 Socket接口函数 Socket编程的一般流程如下 1、 socket int socket(int domain, int type, int protocol); 创建一个Socket domain即协议域又称为协议族 family 。常用的协议族有 AF_INET 、 AF_INET6 、 AF_LOCAL 或称 AF_UNIX Unix 域 socket 、 AF_ROUTE 等等。协议族决定了 socket 的地址类型在通信中必须采用对应的地址如 AF_INET 决定了要用 ipv4 地址 32 位的与端口号 16 位的的组合、 AF_UNIX 决定了要用一个绝对路径名作为地址。 type指定 socket 类型。常用的 socket 类型有 SOCK_STREAM 、 SOCK_DGRAM 、 SOCK_RAW 、 SOCK_PACKET 、 SOCK_SEQPACKET 等等。 protocol指定协议。常用的协议有 IPPROTO_TCP 、 IPPTOTO_UDP 、 IPPROTO_SCTP 、 IPPROTO_TIPC 等它们分别对应 TCP 传输协议、 UDP 传输协议、 STCP 传输协议、 TIPC 传输协议 。 当 protocol 为 0 时会自动选择 type 类型对应的默认协议 。 2、 bind int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 把一个地址族中的特定地址赋给so cket sockfd即 socket 描述字通过 socket 函数创建 得到 唯一标识一个 socket 。 addrconst struct sockaddr * 指针指向要绑定给 sockfd 的协议地址。 IPV4的协议地址结构 struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */struct in_addr { uint32_t s_addr; /* address in network byte order */ }; IPV6的协议地址结构 struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ }; struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ }; 3、 listen int listen(int sockfd, int backlog); 设置sockfd 套接字为监听套接字 sockfd 参数即为要监听的 socket 描述字 backlog 参数为相应 socket 可以排队的最大连接个数。 socket函数创建的 socket 默认是一个主动类型的 listen 函数将 socket 变为被动类型的等待客户的连接请求。 4、 accept int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 接收客户端的请求建立连接套接字 参数sockfd 是监听套接字用来监听一个端口 参数addr 用来接 收客户端的协议地址可以设置为NULL 。 参数len 表示接收的客户端的协议地址addr结构的大小的可设置为 NULL 如果 accept 成功返回则服务器与客户已经正确建立连接了服务器通过 accept 返回的套接字来完成与客户的通信。 accept默认会阻塞进程直到有一个客户连接建立后返回返回的是一个新可用的连接套接字。 监听套接字: 在调用 listen 函数之后 socket 函数生成的 主动连接的普通套接字就转变为 监听套接字 一般被 accept 函数调用的 sockfd 就是监听套接字 连接套接字 accept 函数返回的是连接 套接字 代表 与客户端已经建立连接 一个服务器 程序 通常只创建一个监听套接字在服务器程序的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个连接套接字当服务器完成了对某个客户的服务相应的连接套接字就被关闭。 连接套接字 并没有占用新的端口与客户端通信依然使用的是与监听套接字sockfd 一样的端口号 。 5、 connect int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); sockfd 参数即为客户端的 socket 描述字 addr 参数为服务器的 socket 地址 addrlen 参数为 socket 地址的长度 成功执行后 客户端通过调用 connect 函数来建立与 TCP 服务器的连接。 6、数据传输操作 ssize_t read(int fd, void *buf, size_t count); read函数是负责从 连接套接字 fd中读取内容 。 fd参数是 accept 函数建立的连接套接字 buf参数是读取的内容存放的内存缓冲区 count参数是要读取的内容的大小 当读成功时 read 返回实际所读的字节数如果返回的值是 0 表示已经读到文件的结束小于 0 表示出现了错误。如果错误为 EINTR 说明读是由中断引起的如果是 ECONNREST 表示网络连接出了问题。 ssize_t write(int fd, const void *buf, size_t count); write函数是向连接套接字 fd 写入内容 fd参数表示建立的连接套接字 buf参数表示要写入内容所在的内存缓冲区 count参数表示要写入的内容的大小 ssize_t send(int sockfd, const void *buf, size_t len, int flags); send函数向连接套接字 sockfd 发送内容 sockfd参数表示发送到的连接套接字 buf参数表示要发送的内容所在的内存缓冲区 len参数表示要发送内容的长度 flags参数表示 send 的标识符一般为 0 成功返回实际发送的字节数出错返回-1 ssize_t recv(int sockfd, void *buf, size_t len, int flags); recv函数从连接套接字 sockfd 接收内容 sockfd参数表示从哪个连接套接字接收内容 buf参数表示接收的内容存放的内存缓冲区 len参数表示接收内容的实际字节数 flags参数表示 recv 操作标识符一般为 0 ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); sendmsg函数向连接套接字 sockfd 发送信息 sockfd参数表示向哪个连接套接字发送信息 msg参数表示要发送的信息的内存缓冲区 flags参数表示 sendmsg 函数操作的标识一般为 0 MSG_DONTWAIT 表示非阻塞模式 MSG_OOB 表示发送带外数据 struct msghdr { void *msg_name; /* optional address */ socklen_t msg_namelen; /* size of address */ struct iovec *msg_iov; /* scatter/gather array */ size_t msg_iovlen; /* # elements in msg_iov */ void *msg_control; /* ancillary data, see below */ socklen_t msg_controllen; /* ancillary data buffer len */ int msg_flags; /* flags on received message */ }; ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); recvmsg函数从连接套接字scokfd 接收信息 sockfd参数表示从哪个连接套接字接收信息 msg参数表示接收的信息存放的内存缓冲区 flags参数表示 recvmsg 函数操作的标识符 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); sendto函数表示向连接套接字发送内容 sockfd参数表示向哪个连接套接字发送信息 buf参数表示发送的内容所在的内存缓冲区 len参数表示发送的信息的字节数 flags参数表示 sendto 函数的操作标识符 dest_addr参数表示发送到的地址的指针 addrlen参数表示发送到的地址的长度 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); recvfrom函数从连接套接字sockfd 接收信息 sockfd参数表示从哪个连接套接字接收信息 buf参数表示接收的信息存放的内存缓冲区 len参数表示接收的实际字节数 flags参数表示 recvfrom 函数的操作标识符 src_addr参数表示接收的信息来自的 主机协议地址所存放的内存缓冲区 addrlen参数表示接收信息的源主机协议地址的长度 7、 close int close(int fd); 关闭断开连接套接字 fd参数表示要断开的连接套接字 四、 程序实例 服务端server.c #include #include #include #include #include #include #include #include #include #include #define PORT 8888#define LISTEN_QUEUE 10#define BUFFER_SIZE 1024 int main(){ ///定义sockfd int listenfd = socket(AF_INET,SOCK_STREAM, 0); ///定义sockaddr_in struct sockaddr_in server_sockaddr; bzero(&server_sockaddr, sizeof(server_sockaddr)); server_sockaddr.sin_family = AF_INET; server_sockaddr.sin_port = htons(PORT); server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); ///bind成功返回0出错返回-1 if(bind(listenfd, (struct sockaddr *)&server_sockaddr, sizeof(server_sockaddr))==-1) { fprintf(stderr, "bind function failed.
"); exit(-1); } ///listen成功返回0出错返回-1 if(listen(listenfd,LISTEN_QUEUE) == -1) { fprintf(stderr, "listen function failed.
"); exit(-1); } fprintf(stdout, "listening on %d
", PORT); ///客户端套接字 char recvbuf[BUFFER_SIZE]; char sendbuf[BUFFER_SIZE]; struct sockaddr_in client_addr; socklen_t length = sizeof(client_addr); bzero(&client_addr, sizeof(client_addr)); ///成功返回非负描述字出错返回-1 int connsockfd = accept(listenfd, (struct sockaddr*)&client_addr, &length); if(connsockfd<0) { fprintf(stderr, "connect function failed.
"); exit(-1); } while(1) { bzero(recvbuf, sizeof(recvbuf)); bzero(sendbuf, sizeof(sendbuf)); int len = recvfrom(connsockfd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&client_addr, &len); if(strcmp(recvbuf,"exit
")==0) break; if(strcmp(recvbuf,"q
")==0) break; if(strcmp(recvbuf,"quit
")==0) break; fprintf(stdout, "have a new client:%s
", inet_ntoa(client_addr.sin_addr)); fprintf(stdout, "message: %s
", recvbuf); strcpy(sendbuf, recvbuf); send(connsockfd, sendbuf, len, 0); } close(connsockfd); close(listenfd); return 0;}
客户端client.c #include #include #include #include #include #include #include #include #include #include #define PORT 8888#define BUFFER_SIZE 1024 int main(){ ///定义sockfd int clientsockfd = socket(AF_INET, SOCK_STREAM, 0); ///定义sockaddr_in struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(PORT); ///服务器端口 servaddr.sin_addr.s_addr = inet_addr("192.168.0.200"); ///服务器ip ///连接服务器成功返回0错误返回-1 if (connect(clientsockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { fprintf(stderr, "connect function failed.
"); exit(-1); } char sendbuf[BUFFER_SIZE]; char recvbuf[BUFFER_SIZE]; bzero(sendbuf, sizeof(sendbuf)); bzero(recvbuf, sizeof(recvbuf)); while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) { send(clientsockfd, sendbuf, strlen(sendbuf),0); ///发送 if(strcmp(sendbuf,"exit
")==0) break; if(strcmp(sendbuf,"q
")==0) break; if(strcmp(sendbuf,"quit
")==0) break; recv(clientsockfd, recvbuf, sizeof(recvbuf),0); ///接收 fprintf(stdout, "%s
", recvbuf);bzero(sendbuf, sizeof(sendbuf)); bzero(recvbuf, sizeof(recvbuf)); } close(clientsockfd); return 0;}
本文出自 “
生命不息,奋斗不止 ” 博客,转载请与作者联系!
打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮