Socket编程在嵌入式中很重要!看工程师剖析Socket(超详细,附代码)
The following article is from 嵌入式大杂烩 Author 正念君
TCP、UDP通信总结
一、什么是socket?
Socket的英文原义是“孔”或“插座”。在编程中,Socket被称做 套接字
,是网络通信中的一种约定。Socket编程的应用无处不在,我们平时用的QQ、微信、浏览器等程序,都与Socket编程有关。我们平时使用浏览器查资料,这个过程的技术原理是怎样的呢?
我们平时使用浏览器,大致就是这样的一个过程。这里有两个重要的名词: 服务端
与 客户端
。
Socket编程的目的就是如何实现这两端之间的通信。
1、Socket编程在嵌入式中也很重要
Socket编程不仅仅在互联网方面很重要,在我们的嵌入式方面也是非常的重要,因为现在很多电子设备都趋向于联网。比如很多嵌入式工作的招聘要求都会有这一条要求:
说一点题外话,还在学校的朋友,如果感觉到很迷茫,不知道学什么的时候,可以上招聘网站上看看自己未来工作相关的职位的任职要求,这样就可以总结自己的一些不足、比较有针对性的去学习。
二、Socket编程中的几个重要概念
Socket编程用于解决我们 客户端
与 服务端
之间通信的问题。我们平时多多少少都有听过IP地址、端口、TCP协议、UDP协议等概念,这些都与Socket编程中相关,想要知道怎么用起来,当然得先了解它们的一些介绍。下面看一下这些专业术语的一些要点介绍:
1、什么是IP地址?
IP地址(InternetProtocolAddress)
是指互联网协议地址,又译为 网际协议地址
。IP地址被用来给Internet上的电脑一个编号。我们可以把“个人电脑”比作“一台电话”,那么“IP地址”就相当于“电话号码”。若计算机1知道计算机2的IP地址,则计算机1就能访问计算机2。
IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用 点分十进制
表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。
IP地址有 IPv4
与 IPv6
之分,现在用得较多的是IPv4。其中,有一个特殊的IP地址需要我们记住: 127.0.0.1
,这是回送地址,即本地机,一般用来测试使用。后边我们的实例中会用到。
关于IP地址还有很多知识要点,但是对于在Socket编程中的应用,我们暂且知道这么多就可以。
2、什么是TCP/IP端口?
上一点中我们提到,若计算机1知道计算机2的IP地址,则计算机1就能访问计算机2。但是,我们要访问计算机2中的不同的应用软件,则还得需要一个信息: 端口
。端口使用16bit进行编号,即其范围为: 0~65536
。但 0~1023
的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21等。
3、什么是协议?
协议(Protocol)是通信双方进行数据交互的一种约定。如TCP、UDP协议:
(1)TCP协议
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,数据可以准确发送,数据丢失会重发。TCP协议常用于web应用中。
TCP连接(三次握手)
TCP传输起始时,客户端、服务端要完成三次数据交互工作才能建立连接,常称为三次握手。可形象比喻为如下对话:
客户端:服务端您好,我有数据要发给你,请求您开通访问权限。
服务端:客户端您好,已给您开通权限,您可以发送数据了。
客户端:收到,谢谢。
具体示意图为:
这里的SYN和ACK是都是标志位,其中SYN代表新建一个连接,ACK代表确认。其中m、n都是随机数。具体说明如:
第一次握手:SYN标志位被置位,客户端向服务端发送一个随机数m。
第二次握手:ACK、SYN标志位被置位。服务端向客户端发送m+1表示确认刚才收到的数据,同时向客户端发送一个随机数n。
第三次握手:ACK标志被置位。客户端向服务端发送n+1表示确认收到数据。
TCP断开(四次挥手)
TCP断开连接时,客户端、服务端要完成四次数据交互工作才能建立连接,常称为四次挥手。可形象比喻为如下对话:
客户端:服务端您好,我发送数据完毕了,即将和您断开连接。
服务端:客户端您好,我稍稍准备一下,再给您断开
服务端:客户端您好,我准备好了,您可以断开连接了。
客户端:好的,合作愉快!
具体示意图为:
这里的FIN也是一个标志位,代表断开连接。具体说明类似三次握手。
为什么建立连接只需要三次数据交互,而断开连接需要四次呢?
建立连接时,服务端在监听状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。
(2)UDP协议
UDP(User Datagram Protocol, 用户数据报协议)是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,可以保证通讯效率,传输延时小。例如视频聊天应用中用的就是UDP协议,这样可以保证及时丢失少量数据,视频的显示也不受很大影响。
4、什么是协议族?
协议族是多个协议的统称。比如我们的TCP/IP协议族,其不仅仅是TCP协议、IP协议,而是多个协议的集合,其包含IP、TCP、UDP、FTP、SMTP等协议。
三、socket编程的API接口
1、Linux下的socket API接口
(1)创建socket:socket()函数
函数原型:
int socket(int af, int type, int protocol);
af参数:af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AFINET 和 AFINET6,其前缀也可以是PF(Protocol Family),即PFINET 和 PFINET6。
type参数:type 为数据传输方式,常用的有 面向连接(
SOCK_STREAM
)方式(即TCP) 和 无连接(SOCK_DGRAM
)的方式(即UDP)。protocol参数:protocol 表示传输协议,常用的有
IPPROTO_TCP
和IPPTOTO_UDP
,分别表示 TCP 传输协议和 UDP 传输协议。
使用示例:
创建TCP套接字:
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
创建UDP套接字:
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
(2)绑定套接字:bind()函数
函数原型:
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
sock参数:sock 为 socket 文件描述符。
addr参数:addr 为 sockaddr 结构体变量的指针。
addrlen参数:addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。
使用示例:
将创建的套接字 ServerSock
与本地IP 127.0.0.1
、端口 1314
进行绑定:
/* 创建服务端socket */
intServerSock= socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
/* 设置服务端信息 */
struct sockaddr_in ServerSockAddr;
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); // 给结构体ServerSockAddr清零
ServerSockAddr.sin_family = PF_INET; // 使用IPv4地址
ServerSockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");// 本机IP地址
ServerSockAddr.sin_port = htons(1314); // 端口
/* 绑定套接字 */
bind(ServerSock, (SOCKADDR*)&ServerSockAddr, sizeof(SOCKADDR));
其中 structsockaddr_in
类型的结构体变量用于保存IPv4的IP信息。若是IPv6,则有对应的结构体:
struct sockaddr_in6
{
sa_family_t sin6_family; // 地址类型,取值为AF_INET6
in_port_t sin6_port; // 16位端口号
uint32_t sin6_flowinfo; // IPv6流信息
struct in6_addr sin6_addr; // 具体的IPv6地址
uint32_t sin6_scope_id; // 接口范围ID
};
(3)建立连接:connect()函数
函数原型:
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
参数与 bind()
的参数类似。
使用示例:
intClientSock= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
connect(ClientSock, (SOCKADDR*)&ServerSockAddr, sizeof(SOCKADDR));
(4)监听:listen()函数
函数原型:
int listen(int sock, int backlog);
sock参数:sock 为需要进入监听状态的套接字。
backlog参数:backlog 为请求队列的最大长度。
使用示例:
/* 进入监听状态 */
listen(ServerSock, 10);
(5)接收请求:accept()函数
函数原型:
int accept(int sock, struct sockaddr *addr, socklen_t*addrlen);
sock参数:sock 为服务器端套接字。
addr参数:addr 为 sockaddr_in 结构体变量。
addrlen参数:addrlen 为参数 addr 的长度,可由 sizeof() 求得。
返回值:一个新的套接字,用于与客户端通信。
使用示例:
/* 监听客户端请求,accept函数返回一个新的套接字,发送和接收都是用这个套接字 */
intClientSock= accept(ServerSock, (SOCKADDR*)&ClientAddr, &len);
(6)关闭:close()函数
函数原型:
int close(int fd);
fd:要关闭的文件描述符。
使用示例:
close(ServerSock);
(7)数据的接收和发送
数据收发函数有几组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
函数原型:
ssize_t read(int fd, void*buf, size_t count);
ssize_t write(int fd, constvoid*buf, size_t count);
ssize_t send(int sockfd, constvoid*buf, size_t len, int flags);
ssize_t recv(int sockfd, void*buf, size_t len, int flags);
ssize_t sendto(int sockfd, constvoid*buf, size_t len, int flags,
conststruct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void*buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t*addrlen);
ssize_t sendmsg(int sockfd, conststruct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
这里介绍一下recv()/send()、recvfrom()/sendto()。
recv()函数:
ssize_t recv(int sockfd, void*buf, size_t len, int flags);
sockfd参数:sockfd为要接收数据的套接字。
buf参数:buf 为要接收的数据的缓冲区地址。
len参数:len 为要接收的数据的字节数。
flags参数:flags 为接收数据时的选项,常设为0。
send()函数:
ssize_t send(int sockfd, constvoid*buf, size_t len, int flags);
sockfd参数:sockfd为要发送数据的套接字。
buf参数:buf 为要发送的数据的缓冲区地址。
len参数:len 为要发送的数据的字节数。
flags参数:flags 为发送数据时的选项,常设为0。
recvfrom()函数:
ssize_t recvfrom(int sock, void*buf, size_t nbytes, int flags, struct sockadr *from, socklen_t*addrlen);
sock:用于接收UDP数据的套接字;
buf:保存接收数据的缓冲区地址;
nbytes:可接收的最大字节数(不能超过buf缓冲区的大小);
flags:可选项参数,若没有可传递0;
from:存有发送端地址信息的sockaddr结构体变量的地址;
addrlen:保存参数 from 的结构体变量长度的变量地址值。
sendto()函数:
ssize_t sendto(int sock, void*buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
sock:用于传输UDP数据的套接字;
buf:保存待传输数据的缓冲区地址;
nbytes:带传输数据的长度(以字节计);
flags:可选项参数,若没有可传递0;
to:存有目标地址信息的 sockaddr 结构体变量的地址;
addrlen:传递给参数 to 的地址值结构体变量的长度。
2、windows下的socket API接口
跟Linux下的差不多:
SOCKET socket(int af, int type, int protocol);
int bind(SOCKET sock, conststruct sockaddr *addr, int addrlen);
int connect(SOCKET sock, conststruct sockaddr *serv_addr, int addrlen);
int listen(SOCKET sock, int backlog);
SOCKET accept(SOCKET sock, struct sockaddr *addr, int*addrlen);
int closesocket( SOCKET s);
int send(SOCKET sock, constchar*buf, int len, int flags);
int recv(SOCKET sock, char*buf, int len, int flags);
int recvfrom(SOCKET sock, char*buf, int nbytes, int flags, conststruct sockaddr *from, int*addrlen);
int sendto(SOCKET sock, constchar*buf, int nbytes, int flags, conststruct sockadr *to, int addrlen);
3、TCP、UDP通信的socket编程过程图
(1)TCP通信socket编程过程
(2)UDP通信socket编程过程
四、socket的应用实例
1、基于TCP的本地客户端、服务端信息交互实例
本例的例子实现的功能为:本地TCP客户端往本地TCP服务端发送数据,TCP服务端收到数据则会打印输出,同时把原数据返回给TCP客户端。这个例子类似于我们在做单片机的串口实验时,串口上位机往我们的单片机发送数据,单片机收到数据则把该数据原样返回给上位机。
(1)windows的程序:
服务端程序tcp_server.c:
#include<stdio.h>
#include<winsock2.h>
#define BUF_LEN 100
int main(void)
{
WSADATA wd;
SOCKET ServerSock, ClientSock;
charBuf[BUF_LEN] = {0};
SOCKADDR ClientAddr;
SOCKADDR_IN ServerSockAddr;
int addr_size = 0, recv_len = 0;
/* 初始化操作sock需要的DLL */
WSAStartup(MAKEWORD(2,2),&wd);
/* 创建服务端socket */
if(-1== (ServerSock= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
{
printf("socket error!\n");
exit(1);
}
/* 设置服务端信息 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); // 给结构体ServerSockAddr清零
ServerSockAddr.sin_family = AF_INET; // 使用IPv4地址
ServerSockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");// 本机IP地址
ServerSockAddr.sin_port = htons(1314); // 端口
/* 绑定套接字 */
if(-1== bind(ServerSock, (SOCKADDR*)&ServerSockAddr, sizeof(SOCKADDR)))
{
printf("bind error!\n");
exit(1);
}
/* 进入监听状态 */
if(-1== listen(ServerSock, 10))
{
printf("listen error!\n");
exit(1);
}
addr_size = sizeof(SOCKADDR);
while(1)
{
/* 监听客户端请求,accept函数返回一个新的套接字,发送和接收都是用这个套接字 */
if(-1== (ClientSock= accept(ServerSock, (SOCKADDR*)&ClientAddr, &addr_size)))
{
printf("socket error!\n");
exit(1);
}
/* 接受客户端的返回数据 */
int recv_len = recv(ClientSock, Buf, BUF_LEN, 0);
printf("客户端发送过来的数据为:%s\n", Buf);
/* 发送数据到客户端 */
send(ClientSock, Buf, recv_len, 0);
/* 关闭客户端套接字 */
closesocket(ClientSock);
/* 清空缓冲区 */
memset(Buf, 0, BUF_LEN);
}
/*如果有退出循环的条件,这里还需要清除对socket库的使用*/
/* 关闭服务端套接字 */
//closesocket(ServerSock);
/* WSACleanup();*/
return0;
}
客户端程序tcp_client.c:
#include<stdio.h>
#include<winsock2.h>
#define BUF_LEN 100
int main(void)
{
WSADATA wd;
SOCKET ClientSock;
charBuf[BUF_LEN] = {0};
SOCKADDR_IN ServerSockAddr;
/* 初始化操作sock需要的DLL */
WSAStartup(MAKEWORD(2,2),&wd);
/* 向服务器发起请求 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr));
ServerSockAddr.sin_family = AF_INET;
ServerSockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ServerSockAddr.sin_port = htons(1314);
while(1)
{
/* 创建客户端socket */
if(-1== (ClientSock= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
{
printf("socket error!\n");
exit(1);
}
if(-1== connect(ClientSock, (SOCKADDR*)&ServerSockAddr, sizeof(SOCKADDR)))
{
printf("connect error!\n");
exit(1);
}
printf("请输入一个字符串,发送给服务端:");
gets(Buf);
/* 发送数据到服务端 */
send(ClientSock, Buf, strlen(Buf), 0);
/* 接受服务端的返回数据 */
recv(ClientSock, Buf, BUF_LEN, 0);
printf("服务端发送过来的数据为:%s\n", Buf);
memset(Buf, 0, BUF_LEN); // 重置缓冲区
closesocket(ClientSock); // 关闭套接字
}
// WSACleanup(); /*如果有退出循环的条件,这里还需要清除对socket库的使用*/
return0;
}
我们上边的IP地址概念那一部分中,有强调 127.0.0.1
这个IP是一个特殊的IP地址,这是回送地址,即本地机,一般用来测试使用。这个例子中我们就用到了。此外,端口我们设置为 1314
,这是随意设置的,只要范围在 1024~65536
之间就可以。
本文使用的是gcc编译器编译(关于gcc编译器的相关介绍可查看往期笔记:【C语言笔记】使用notepad++、MinGW来开发C程序及【C语言笔记】windows命令行下编译C程序),编译命令如下:
gcc tcp_client.c -o tcp_client.exe -lwsock32
gcc tcp_server.c -o tcp_server.exe -lwsock32
这里必须要加 -lwsock32
这个参数用于链接windows下socket编程必须的winsock2这个库。若是使用集成开发环境,则需要把 wsock32.lib
放在工程目录下,并在我们代码中 #include<winsock2.h>
下面加上一行 #pragmacomment(lib,"ws2_32.lib")
代码(这种情况本人未验证,有兴趣的朋友可尝试)。
实验现象:
先启动服务端程序 tcp_server.exe
,再启动客户端程序 tcp_client.exe
,并在客户端中输入字符串,则当服务端会接收到字符串时会打印输出,与此同时也会往客户端返回相同的数据:
动图:
(2)Linux的程序:
在linux下,“一切都是文件”,所以这里我们的套接字也当做文件来看待。
服务端程序linux_tcp_server.c:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define BUF_LEN 100
int main(void)
{
intServerFd, ClientFd;
charBuf[BUF_LEN] = {0};
struct sockaddr ClientAddr;
int addr_len = 0, recv_len = 0;
struct sockaddr_in ServerSockAddr;
int optval = 1;
/* 创建服务端文件描述符 */
if(-1== (ServerFd= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
{
printf("socket error!\n");
exit(1);
}
/* 设置服务端信息 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); // 给结构体ServerSockAddr清零
ServerSockAddr.sin_family = AF_INET; // 使用IPv4地址
ServerSockAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 自动获取IP地址
ServerSockAddr.sin_port = htons(6666); // 端口
// 设置地址和端口号可以重复使用
if(setsockopt(ServerFd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
printf("setsockopt error!\n");
exit(1);
}
/* 绑定操作,绑定前加上上面的socket属性可重复使用地址 */
if(-1== bind(ServerFd, (struct sockaddr*)&ServerSockAddr, sizeof(struct sockaddr)))
{
printf("bind error!\n");
exit(1);
}
/* 进入监听状态 */
if(-1== (listen(ServerFd, 10)))
{
printf("listen error!\n");
exit(1);
}
addr_len = sizeof(struct sockaddr);
while(1)
{
/* 监听客户端请求,accept函数返回一个新的套接字,发送和接收都是用这个套接字 */
if(-1== (ClientFd= accept(ServerFd, (struct sockaddr*)&ClientAddr, &addr_len)))
{
printf("accept error!\n");
exit(1);
}
/* 接受客户端的返回数据 */
if((recv_len = recv(ClientFd, Buf, BUF_LEN, 0)) < 0)
{
printf("recv error!\n");
exit(1);
}
printf("客户端发送过来的数据为:%s\n", Buf);
/* 发送数据到客户端 */
send(ClientFd, Buf, recv_len, 0);
/* 关闭客户端套接字 */
close(ClientFd);
/* 清空缓冲区 */
memset(Buf, 0, BUF_LEN);
}
return0;
}
客户端程序linux_tcp_client.c:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#define BUF_LEN 100
int main(void)
{
intClientFd;
charBuf[BUF_LEN] = {0};
struct sockaddr_in ServerSockAddr;
/* 向服务器发起请求 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr));
ServerSockAddr.sin_family = AF_INET;
ServerSockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ServerSockAddr.sin_port = htons(6666);
while(1)
{
/* 创建客户端socket */
if(-1== (ClientFd= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
{
printf("socket error!\n");
exit(1);
}
/* 连接 */
if(-1== connect(ClientFd, (struct sockaddr*)&ServerSockAddr, sizeof(ServerSockAddr)))
{
printf("connect error!\n");
exit(1);
}
printf("请输入一个字符串,发送给服务端:");
gets(Buf);
/* 发送数据到服务端 */
send(ClientFd, Buf, strlen(Buf), 0);
memset(Buf, 0, BUF_LEN); // 重置缓冲区
/* 接受服务端的返回数据 */
recv(ClientFd, Buf, BUF_LEN, 0);
printf("服务端发送过来的数据为:%s\n", Buf);
memset(Buf, 0, BUF_LEN); // 重置缓冲区
close(ClientFd); // 关闭套接字
}
return0;
}
Linux下编译就不需要添加 -lwsock32
参数:
gcc linux_tcp_server.c -o linux_tcp_server
gcc linux_tcp_client.c -o linux_tcp_client
实验现象:
在调试这份程序时,出现了绑定错误:
经上网查询发现是端口重复使用,可以在调用 bind()
函数之前调用 setsockopt()
函数以解决端口重复使用的问题:
2、基于UDP的本地客户端、服务端信息交互实例
(1)windows的程序
服务端程序udp_server.c:
#include<stdio.h>
#include<winsock2.h>
#define BUF_LEN 100
int main(void)
{
WSADATA wd;
SOCKET ServerSock;
charBuf[BUF_LEN] = {0};
SOCKADDR ClientAddr;
SOCKADDR_IN ServerSockAddr;
int addr_size = 0;
/* 初始化操作sock需要的DLL */
WSAStartup(MAKEWORD(2,2),&wd);
/* 创建服务端socket */
if(-1== (ServerSock= socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)))
{
printf("socket error!\n");
exit(1);
}
/* 设置服务端信息 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); // 给结构体ServerSockAddr清零
ServerSockAddr.sin_family = AF_INET; // 使用IPv4地址
ServerSockAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 自动获取IP地址
ServerSockAddr.sin_port = htons(1314); // 端口
/* 绑定套接字 */
if(-1== (bind(ServerSock, (SOCKADDR*)&ServerSockAddr, sizeof(SOCKADDR))))
{
printf("bind error!\n");
exit(1);
}
addr_size = sizeof(SOCKADDR);
while(1)
{
/* 接受客户端的返回数据 */
int str_len = recvfrom(ServerSock, Buf, BUF_LEN, 0, &ClientAddr, &addr_size);
printf("客户端发送过来的数据为:%s\n", Buf);
/* 发送数据到客户端 */
sendto(ServerSock, Buf, str_len, 0, &ClientAddr, addr_size);
/* 清空缓冲区 */
memset(Buf, 0, BUF_LEN);
}
/*如果有退出循环的条件,这里还需要清除对socket库的使用*/
/* 关闭服务端套接字 */
//closesocket(ServerSock);
/* WSACleanup();*/
return0;
}
客户端程序udp_client.c:
#include<stdio.h>
#include<winsock2.h>
#define BUF_LEN 100
int main(void)
{
WSADATA wd;
SOCKET ClientSock;
charBuf[BUF_LEN] = {0};
SOCKADDR ServerAddr;
SOCKADDR_IN ServerSockAddr;
intServerAddrLen= 0;
/* 初始化操作sock需要的DLL */
WSAStartup(MAKEWORD(2,2),&wd);
/* 创建客户端socket */
if(-1== (ClientSock= socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)))
{
printf("socket error!\n");
exit(1);
}
/* 向服务器发起请求 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr));
ServerSockAddr.sin_family = PF_INET;
ServerSockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ServerSockAddr.sin_port = htons(1314);
ServerAddrLen= sizeof(ServerAddr);
while(1)
{
printf("请输入一个字符串,发送给服务端:");
gets(Buf);
/* 发送数据到服务端 */
sendto(ClientSock, Buf, strlen(Buf), 0, (struct sockaddr*)&ServerSockAddr, sizeof(ServerSockAddr));
/* 接受服务端的返回数据 */
recvfrom(ClientSock, Buf, BUF_LEN, 0, &ServerAddr, &ServerAddrLen);
printf("服务端发送过来的数据为:%s\n", Buf);
memset(Buf, 0, BUF_LEN); // 重置缓冲区
}
closesocket(ClientSock); // 关闭套接字
// WSACleanup(); /*如果有退出循环的条件,这里还需要清除对socket库的使用*/
return0;
}
(2)Linux下的程序
服务端程序linux_udp_server.c:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define BUF_LEN 100
int main(void)
{
intServerFd;
charBuf[BUF_LEN] = {0};
struct sockaddr ClientAddr;
struct sockaddr_in ServerSockAddr;
int addr_size = 0;
int optval = 1;
/* 创建服务端socket */
if( -1== (ServerFd= socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)))
{
printf("socket error!\n");
exit(1);
}
/* 设置服务端信息 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); // 给结构体ServerSockAddr清零
ServerSockAddr.sin_family = AF_INET; // 使用IPv4地址
ServerSockAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 自动获取IP地址
ServerSockAddr.sin_port = htons(1314); // 端口
// 设置地址和端口号可以重复使用
if(setsockopt(ServerFd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
printf("setsockopt error!\n");
exit(1);
}
/* 绑定操作,绑定前加上上面的socket属性可重复使用地址 */
if(-1== bind(ServerFd, (struct sockaddr*)&ServerSockAddr, sizeof(ServerSockAddr)))
{
printf("bind error!\n");
exit(1);
}
addr_size = sizeof(ClientAddr);
while(1)
{
/* 接受客户端的返回数据 */
int str_len = recvfrom(ServerFd, Buf, BUF_LEN, 0, &ClientAddr, &addr_size);
printf("客户端发送过来的数据为:%s\n", Buf);
/* 发送数据到客户端 */
sendto(ServerFd, Buf, str_len, 0, &ClientAddr, addr_size);
/* 清空缓冲区 */
memset(Buf, 0, BUF_LEN);
}
close(ServerFd);
return0;
}
客户端程序linux_udp_client.c:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#define BUF_LEN 100
int main(void)
{
intClientFd;
charBuf[BUF_LEN] = {0};
struct sockaddr ServerAddr;
int addr_size = 0;
struct sockaddr_in ServerSockAddr;
/* 创建客户端socket */
if(-1== (ClientFd= socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)))
{
printf("socket error!\n");
exit(1);
}
/* 向服务器发起请求 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr));
ServerSockAddr.sin_family = PF_INET;
ServerSockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ServerSockAddr.sin_port = htons(1314);
addr_size = sizeof(ServerAddr);
while(1)
{
printf("请输入一个字符串,发送给服务端:");
gets(Buf);
/* 发送数据到服务端 */
sendto(ClientFd, Buf, strlen(Buf), 0, (struct sockaddr*)&ServerSockAddr, sizeof(ServerSockAddr));
/* 接受服务端的返回数据 */
recvfrom(ClientFd, Buf, BUF_LEN, 0, &ServerAddr, &addr_size);
printf("服务端发送过来的数据为:%s\n", Buf);
memset(Buf, 0, BUF_LEN); // 重置缓冲区
}
close(ClientFd); // 关闭套接字
return0;
}
实验现象:
实验现象如实例1。
五、总结
本笔记简单介绍了一些与socket编程相关的一些知识点:IP地址,什么是端口,协议等。重点介绍了TCP、UDP通信的一些原理及其API接口的用法,并给出了windows和linux下的TCP、UDP通信实例。以上就是关于socket编程的一些总结,如有错误,欢迎指出!
下一篇分享一个基于TCP的HTTP客户端的实例,欢迎持续关注!
第二章
基于C语言的天气客户端的实现
一、前言
上一篇笔记分享了【socket笔记】TCP、UDP通信总结,这一篇分享一个用C语言写的、基于TCP的一个HTTP天气客户端的实现,这个一个控制台应用程序,最终的界面如下:
关于天气预报,之前我已经用STM32+ESP8266
wifi模块实现过了一遍,感兴趣的可查阅往期笔记:基于STM32的智能天气预报系统。这次这个基于C语言控制台程序的HTTP客户端的天气解析的代码和之前分享的差不多,只是在那基础上添加修改了一些东西,并配合socket的相关知识实现的,以巩固一下socket编程的知识。下面分享一些实现过程。
二、天气客户端实现的要点
首先,需要说明的是,这份代码是在windows系统下使用gcc6.3.0
进行编译的。
1、秘钥
心知天气:www.seniverse.com
我们完成这个实验必须得到这个上面去注册一个账号才能使用它的天气数据,注册之后每个账户都会有一个私钥
,例如:
私钥 SMEieQjde1C9eXnbE
这个是我们程序中需要用到。
2、IP和端口
上一节分享了socket的笔记,我们与服务端通信,需要知道三个重要的信息,分别是:
IP地址
端口
传输方式
这里的心知天气的IP是116.62.81.138
,端口是80
,传输方式是TCP
,对应的代码如下:
左右滑动查看全部代码>>>
/* 设置要访问的服务器的信息 */
SOCKADDR_IN ServerSockAddr;
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); // 每个字节都用0填充
ServerSockAddr.sin_family = PF_INET; // IPv4
ServerSockAddr.sin_addr.s_addr = inet_addr(WEATHER_IP_ADDR); // 心知天气服务器IP
ServerSockAddr.sin_port = htons(WEATHER_PORT); // 端口
这里的WEATHER_IP_ADDR
对应的就是116.62.81.138
,WEATHER_PORT
对应的就是80
。
3、GET请求
HTTP有几种请求方法,我们这里使用的是GET
请求。查看心知天气API文档可知,请求地址示例为:
https://api.seniverse.com/v3/weather/now.json?key=your_api_key&location=beijing&language=zh-Hans&unit=c
这是一个天气实况的请求地址示例,其有几个重要的参数:
这里的key
是个很重要的参数,就是我们前面说的私钥
。
我们的天气客户端就是要往天气服务端发送类似这样的GET请求
来获取天气数据,具体的请求方法示例为:
左右滑动查看全部代码>>>
GET https://api.seniverse.com/v3/weather/now.json?key=2owqvhhd2dd9o9f8&location=beijing&language=zh-Hans&unit=c
对应代码如下:
左右滑动查看全部代码>>>
/* 秘钥,注意!!如果要用这一份代码,这个一定要改为自己的,因为这个我已经故意改错了,防止有人与我公用一个KEY */
#define KEY "2owqvhhd2dd9o9f8" // 这是在心知天气注册后,每个用户自己的一个key
/* GET请求包 */
#define GET_REQUEST_PACKAGE \
"GET https://api.seniverse.com/v3/weather/%s.json?key=%s&location=%s&language=zh-Hans&unit=c\r\n\r\n"
/* JSON数据包 */
#define NOW_JSON "now"
#define DAILY_JSON "daily"
//....还用更多其他的天气数据包可查阅心知天气
/* 组合GET请求包 */
sprintf(GetRequestBuf, GET_REQUEST_PACKAGE, weather_json, KEY, location);
/* 发送数据到服务端 */
send(ClientSock, GetRequestBuf, strlen(GetRequestBuf), 0);
这里简单复习一下sprintf函数
的用法:
(1)函数功能:字符串格式化
(2)函数原型:int sprintf(char *string, char *format [,argument,…]);
string:这是指向一个字符数组的指针,该数组存储了 C 字符串。
format :这是字符串,包含了要被写入到字符串 str 的文本。
[argument]...:根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。
(3)使用示例:
sprintf(buf, "%s,%d", str, num);
假如此时str
为"hello"
,num
为5201314
,则此时buf中的内容为:hello,5201314
,需要注意的是buf的容量要足够大。
4、天气服务端返回的数据
天气服务端给我们天气客户端返回的数据为JSON
格式数据,可查阅往期笔记JSON的简单认识。我们这个天气客户端只是实现了查询此刻天气(对应的数据包为now.json
)及近三天天气情况(对应的数据包为daily.json
),如要查询其他信息,可模仿我们这里处理now.json
和daily.json
的方法,我们用cJson库
进行解析。
这个cJson
库的下载链接为:
链接:https://pan.baidu.com/s/1DQynsdlNyIvsVXmf4W5b8Q
提取码:ww4z
只要把cJSON.c
与cJSON.h
放到工程主程序所在目录,然后在主程序中包含头文件JSON.h
即可引入该库:
为了解析now.json
和daily.json
中的有用数据,我们建立如下结构体:
左右滑动查看全部代码>>>
/* 天气数据结构体 */
typedef struct
{
/* 实况天气数据 */
char id[32]; //id
char name[32]; //地名
char country[32]; //国家
char path[32]; //完整地名路径
char timezone[32]; //时区
char timezone_offset[32]; //时差
char text[32]; //天气预报文字
char code[32]; //天气预报代码
char temperature[32]; //气温
char last_update[32]; //最后一次更新的时间
/* 今天、明天、后天天气数据 */
char date[3][32]; //日期
char text_day[3][64]; //白天天气现象文字
char code_day[3][32]; //白天天气现象代码
char code_night[3][64]; //晚间天气现象代码
char high[3][32]; //最高温
char low[3][32]; //最低温
char wind_direction[3][64]; //风向
char wind_speed[3][32]; //风速,单位km/h(当unit=c时)
char wind_scale[3][32]; //风力等级
}Weather;
现在看一下now.json
和daily.json
的内容是怎样的:
(1)now.json
示例及解析:
now.json:
左右滑动查看全部代码>>>
{
"results": [
{
"location": {
"id": "C23NB62W20TF",
"name": "西雅图",
"country": "US",
"path": "西雅图,华盛顿州,美国",
"timezone": "America/Los_Angeles",
"timezone_offset": "-07:00"
},
"now": {
"text": "多云", //天气现象文字
"code": "4", //天气现象代码
"temperature": "14", //温度,单位为c摄氏度或f华氏度
"feels_like": "14", //体感温度,单位为c摄氏度或f华氏度
"pressure": "1018", //气压,单位为mb百帕或in英寸
"humidity": "76", //相对湿度,0~100,单位为百分比
"visibility": "16.09", //能见度,单位为km公里或mi英里
"wind_direction": "西北", //风向文字
"wind_direction_degree": "340", //风向角度,范围0~360,0为正北,90为正东,180为正南,270为正西
"wind_speed": "8.05", //风速,单位为km/h公里每小时或mph英里每小时
"wind_scale": "2", //风力等级,请参考:http://baike.baidu.com/view/465076.htm
"clouds": "90", //云量,单位%,范围0~100,天空被云覆盖的百分比 #目前不支持中国城市#
"dew_point": "-12" //露点温度,请参考:http://baike.baidu.com/view/118348.htm #目前不支持中国城市#
},
"last_update": "2015-09-25T22:45:00-07:00" //数据更新时间(该城市的本地时间)
}
]
}
这里实测了一下,我们普通用户(因为没充钱,哈哈~)申请的now.json
数据中,now对象
中只有如下三个键值对:
左右滑动查看全部代码>>>
"text": "多云", //天气现象文字
"code": "4", //天气现象代码
"temperature": "14", //温度,单位为c摄氏度或f华氏度
now.json的解析函数:
左右滑动查看全部代码>>>
/*******************************************************************************************************
** 函数: cJSON_NowWeatherParse,解析天气实况数据
**------------------------------------------------------------------------------------------------------
** 参数: JSON:天气数据包 result:数据解析的结果
** 返回: void
********************************************************************************************************/
static int cJSON_NowWeatherParse(char *JSON, Weather *result)
{
cJSON *json,*arrayItem,*object,*subobject,*item;
json = cJSON_Parse(JSON); //解析JSON数据包
if(json == NULL) //检测JSON数据包是否存在语法上的错误,返回NULL表示数据包无效
{
printf("Error before: [%s]\n",cJSON_GetErrorPtr()); //打印数据包语法错误的位置
return 1;
}
else
{
if((arrayItem = cJSON_GetObjectItem(json,"results")) != NULL); //匹配字符串"results",获取数组内容
{
int size = cJSON_GetArraySize(arrayItem); //获取数组中对象个数
#if DEBUG
printf("cJSON_GetArraySize: size=%d\n",size);
#endif
if((object = cJSON_GetArrayItem(arrayItem,0)) != NULL)//获取父对象内容
{
/* 匹配子对象1:城市地区相关 */
if((subobject = cJSON_GetObjectItem(object,"location")) != NULL)
{
// 匹配id
if((item = cJSON_GetObjectItem(subobject,"id")) != NULL)
{
memcpy(result->id, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配城市名
if((item = cJSON_GetObjectItem(subobject,"name")) != NULL)
{
memcpy(result->name, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配城市所在的国家
if((item = cJSON_GetObjectItem(subobject,"country")) != NULL)
{
memcpy(result->country, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配完整地名路径
if((item = cJSON_GetObjectItem(subobject,"path")) != NULL)
{
memcpy(result->path, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配时区
if((item = cJSON_GetObjectItem(subobject,"timezone")) != NULL)
{
memcpy(result->timezone, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配时差
if((item = cJSON_GetObjectItem(subobject,"timezone_offset")) != NULL)
{
memcpy(result->timezone_offset, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
}
/* 匹配子对象2:今天的天气情况 */
if((subobject = cJSON_GetObjectItem(object,"now")) != NULL)
{
// 匹配天气现象文字
if((item = cJSON_GetObjectItem(subobject,"text")) != NULL)
{
memcpy(result->text, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配天气现象代码
if((item = cJSON_GetObjectItem(subobject,"code")) != NULL)
{
memcpy(result->code, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配气温
if((item = cJSON_GetObjectItem(subobject,"temperature")) != NULL)
{
memcpy(result->temperature, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
}
/* 匹配子对象3:数据更新时间(该城市的本地时间) */
if((subobject = cJSON_GetObjectItem(object,"last_update")) != NULL)
{
memcpy(result->last_update, subobject->valuestring,strlen(subobject->valuestring)); // 保存数据供外部调用
}
}
}
}
cJSON_Delete(json); //释放cJSON_Parse()分配出来的内存空间
return 0;
}
(2)daily.json
示例及解析:
daily.json:
左右滑动查看全部代码>>>
{
"results": [{
"location": {
"id": "WX4FBXXFKE4F",
"name": "北京",
"country": "CN",
"path": "北京,北京,中国",
"timezone": "Asia/Shanghai",
"timezone_offset": "+08:00"
},
"daily": [{ //返回指定days天数的结果
"date": "2015-09-20", //日期
"text_day": "多云", //白天天气现象文字
"code_day": "4", //白天天气现象代码
"text_night": "晴", //晚间天气现象文字
"code_night": "0", //晚间天气现象代码
"high": "26", //当天最高温度
"low": "17", //当天最低温度
"precip": "0", //降水概率,范围0~100,单位百分比(目前仅支持国外城市)
"wind_direction": "", //风向文字
"wind_direction_degree": "255", //风向角度,范围0~360
"wind_speed": "9.66", //风速,单位km/h(当unit=c时)、mph(当unit=f时)
"wind_scale": "" //风力等级
}, {
"date": "2015-09-21",
"text_day": "晴",
"code_day": "0",
"text_night": "晴",
"code_night": "0",
"high": "27",
"low": "17",
"precip": "0",
"wind_direction": "",
"wind_direction_degree": "157",
"wind_speed": "17.7",
"wind_scale": "3"
}, {
... //更多返回结果
}],
"last_update": "2015-09-20T18:00:00+08:00" //数据更新时间(该城市的本地时间)
}]
}
daily.json解析函数:
左右滑动查看全部代码>>>
/*******************************************************************************************************
** 函数: cJSON_DailyWeatherParse,解析近三天天气数据
**------------------------------------------------------------------------------------------------------
** 参数: JSON:天气数据包 result:数据解析的结果
** 返回: void
********************************************************************************************************/
static int cJSON_DailyWeatherParse(char *JSON, Weather *result)
{
cJSON *json,*arrayItem,*object,*subobject,*item,*sub_child_object,*child_Item;
json = cJSON_Parse(JSON); //解析JSON数据包
if(json == NULL) //检测JSON数据包是否存在语法上的错误,返回NULL表示数据包无效
{
printf("Error before: [%s]\n",cJSON_GetErrorPtr()); //打印数据包语法错误的位置
return 1;
}
else
{
if((arrayItem = cJSON_GetObjectItem(json,"results")) != NULL); //匹配字符串"results",获取数组内容
{
int size = cJSON_GetArraySize(arrayItem); //获取数组中对象个数
#if DEBUG
printf("Get Array Size: size=%d\n",size);
#endif
if((object = cJSON_GetArrayItem(arrayItem,0)) != NULL)//获取父对象内容
{
/* 匹配子对象1------结构体location */
if((subobject = cJSON_GetObjectItem(object,"location")) != NULL)
{
if((item = cJSON_GetObjectItem(subobject,"name")) != NULL) //匹配子对象1成员"name"
{
memcpy(result->name, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
}
/* 匹配子对象2------数组daily */
if((subobject = cJSON_GetObjectItem(object,"daily")) != NULL)
{
int sub_array_size = cJSON_GetArraySize(subobject);
#if DEBUG
printf("Get Sub Array Size: sub_array_size=%d\n",sub_array_size);
#endif
for(int i = 0; i < sub_array_size; i++)
{
if((sub_child_object = cJSON_GetArrayItem(subobject,i))!=NULL)
{
// 匹配日期
if((child_Item = cJSON_GetObjectItem(sub_child_object,"date")) != NULL)
{
memcpy(result->date[i], child_Item->valuestring,strlen(child_Item->valuestring)); // 保存数据
}
// 匹配白天天气现象文字
if((child_Item = cJSON_GetObjectItem(sub_child_object,"text_day")) != NULL)
{
memcpy(result->text_day[i], child_Item->valuestring,strlen(child_Item->valuestring)); // 保存数据
}
// 匹配白天天气现象代码
if((child_Item = cJSON_GetObjectItem(sub_child_object,"code_day")) != NULL)
{
memcpy(result->code_day[i], child_Item->valuestring,strlen(child_Item->valuestring)); // 保存数据
}
// 匹配夜间天气现象代码
if((child_Item = cJSON_GetObjectItem(sub_child_object,"code_night")) != NULL)
{
memcpy(result->code_night[i], child_Item->valuestring,strlen(child_Item->valuestring)); // 保存数据
}
// 匹配最高温度
if((child_Item = cJSON_GetObjectItem(sub_child_object,"high")) != NULL)
{
memcpy(result->high[i], child_Item->valuestring,strlen(child_Item->valuestring)); //保存数据
}
// 匹配最低温度
if((child_Item = cJSON_GetObjectItem(sub_child_object,"low")) != NULL)
{
memcpy(result->low[i], child_Item->valuestring,strlen(child_Item->valuestring)); // 保存数据
}
// 匹配风向
if((child_Item = cJSON_GetObjectItem(sub_child_object,"wind_direction")) != NULL)
{
memcpy(result->wind_direction[i],child_Item->valuestring,strlen(child_Item->valuestring)); //保存数据
}
// 匹配风速,单位km/h(当unit=c时)
if((child_Item = cJSON_GetObjectItem(sub_child_object,"wind_speed")) != NULL)
{
memcpy(result->wind_speed[i], child_Item->valuestring,strlen(child_Item->valuestring)); // 保存数据
}
// 匹配风力等级
if((child_Item = cJSON_GetObjectItem(sub_child_object,"wind_scale")) != NULL)
{
memcpy(result->wind_scale[i], child_Item->valuestring,strlen(child_Item->valuestring)); // 保存数据
}
}
}
}
/* 匹配子对象3------最后一次更新的时间 */
if((subobject = cJSON_GetObjectItem(object,"last_update")) != NULL)
{
//printf("%s:%s\n",subobject->string,subobject->valuestring);
}
}
}
}
cJSON_Delete(json); //释放cJSON_Parse()分配出来的内存空间
return 0;
}
5、获取天气数据并解析
这个函数就涉及到我们上一节笔记中的socket编程的知识了,先看一下这个函数实现的总体框图:
下面是函数实现的细节过程:
左右滑动查看全部代码>>>
/*******************************************************************************************************
** 函数: GetWeather,获取天气数据并解析
**------------------------------------------------------------------------------------------------------
** 参数: weather_json:需要解析的json包 location:地名 result:数据解析的结果
** 返回: void
********************************************************************************************************/
static void GetWeather(char *weather_json, char *location, Weather *result)
{
SOCKET ClientSock;
WSADATA wd;
char GetRequestBuf[256] = {0};
char WeatherRecvBuf[2*1024] = {0};
char GbkRecvBuf[2*1024] = {0};
int gbk_recv_len = 0;
int connect_status = 0;
/* 初始化操作sock需要的DLL */
WSAStartup(MAKEWORD(2,2),&wd);
/* 设置要访问的服务器的信息 */
SOCKADDR_IN ServerSockAddr;
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); // 每个字节都用0填充
ServerSockAddr.sin_family = PF_INET; // IPv4
ServerSockAddr.sin_addr.s_addr = inet_addr(WEATHER_IP_ADDR); // 心知天气服务器IP
ServerSockAddr.sin_port = htons(WEATHER_PORT); // 端口
/* 创建客户端socket */
if (-1 == (ClientSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)))
{
printf("socket error!\n");
exit(1);
}
/* 连接服务端 */
if (-1 == (connect_status = connect(ClientSock, (SOCKADDR*)&ServerSockAddr, sizeof(SOCKADDR))))
{
printf("connect error!\n");
exit(1);
}
/* 组合GET请求包 */
sprintf(GetRequestBuf, GET_REQUEST_PACKAGE, weather_json, KEY, location);
/* 发送数据到服务端 */
send(ClientSock, GetRequestBuf, strlen(GetRequestBuf), 0);
/* 接受服务端的返回数据 */
recv(ClientSock, WeatherRecvBuf, 2*1024, 0);
/* utf-8转为gbk */
SwitchToGbk((const unsigned char*)WeatherRecvBuf, strlen((const char*)WeatherRecvBuf), (unsigned char*)GbkRecvBuf, &gbk_recv_len);
#if DEBUG
printf("服务端返回的数据为:%s\n", GbkRecvBuf);
#endif
/* 解析天气数据并保存到结构体变量weather_data中 */
if (0 == strcmp(weather_json, NOW_JSON)) // 天气实况
{
cJSON_NowWeatherParse(GbkRecvBuf, result);
}
else if(0 == strcmp(weather_json, DAILY_JSON)) // 未来三天天气
{
cJSON_DailyWeatherParse(GbkRecvBuf, result);
}
/* 清空缓冲区 */
memset(GetRequestBuf, 0, 256);
memset(WeatherRecvBuf, 0, 2*1024);
memset(GbkRecvBuf, 0, 2*1024);
/* 关闭套接字 */
closesocket(ClientSock);
/* 终止使用 DLL */
WSACleanup();
}
6、编译
如何编译这份代码(可在文末进行获取)呢?
这份C代码工程的文件如下:
在windows系统下使用gcc
编译器编译,编译命令为:
左右滑动查看全部代码>>>
gcc weather_client.c cJSON.c utf8togbk.c -o weather_client.exe -lwsock32
如:
这里的weather_client.exe
就是我们编译生成的可执行文件:天气客户端
,双击就可以运行了。
此外,-lwsock32
参数上一节也有讲过,这个参数用于链接windows
下socket编程必须的winsock2
这个库。若是使用集成开发环境,则需要把wsock32.lib
放在工程目录下,并在我们代码中#include <winsock2.h>
下面加上一行 #pragma comment(lib, "ws2_32.lib")
代码(在IDE里编译本人未验证,有兴趣的朋友可尝试)。
需要说明的是,Windows下默认是没有装gcc的,需要自己进行配置,关于配置及使用mingw
(这是个工具包,里面包含有gcc编译器)可查看往期笔记:【C语言笔记】使用notepad++、MinGW来开发C程序、【C语言笔记】windows命令行下编译C程序
7、运行结果示例
此处,只能使用拼音进行搜索,其实也可以做输入汉字进行搜索的功能,只是要进行转码处理,这个功能实现在基于STM32的智能天气预报系统的代码里已经有做,有兴趣的朋友可以参考这个。
三、代码获取
第三章
基于Linux、C、JSON、Socket的编程实例
一、前言
之前在学习socket编程的时候有分享一个基于控制台的简易天气客户端的实现,当时提供的是window下的代码,最近有几位小伙伴问有没有Linux版本的。现在就分享Linux版的代码,有需要的朋友自己下载:
链接:https://pan.baidu.com/s/1wiJmdlqTwCxvJlxOY-lAOw
提取码:ngqe
复制这段内容后打开百度网盘手机App,操作更方便哦
之前分享Windows版的时候已经有详细说明整个实现过程了,详细的内容可移步至那篇笔记进行查看。现摘抄一些要点:
二、天气客户端实现的要点
1、秘钥
心知天气:www.seniverse.com
我们完成这个实验必须得到这个上面去注册一个账号才能使用它的天气数据,注册之后每个账户都会有一个私钥
,例如:
私钥 SMEieQjde1C9eXnbE
这个是我们程序中需要用到。
2、IP和端口
我们与服务端通信,需要知道三个重要的信息,分别是:
IP地址
端口
传输方式
这里的心知天气的IP是116.62.81.138
,端口是80
,传输方式是TCP
,对应的代码如下:
左右滑动查看全部代码>>>
/* 设置要访问的服务器的信息 */
struct sockaddr_in ServerSockAddr;
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); // 每个字节都用0填充
ServerSockAddr.sin_family = PF_INET; // IPv4
ServerSockAddr.sin_addr.s_addr = inet_addr(WEATHER_IP_ADDR); // 心知天气服务器IP
ServerSockAddr.sin_port = htons(WEATHER_PORT); // 端口
这里的WEATHER_IP_ADDR
对应的就是116.62.81.138
,WEATHER_PORT
对应的就是80
。
3、GET请求
HTTP有几种请求方法,我们这里使用的是GET
请求。查看心知天气API文档可知,请求地址示例为:
https://api.seniverse.com/v3/weather/now.json?key=your_api_key&location=beijing&language=zh-Hans&unit=c
这是一个天气实况的请求地址示例,其有几个重要的参数:
这里的key
是个很重要的参数,就是我们前面说的私钥
。
我们的天气客户端就是要往天气服务端发送类似这样的GET请求
来获取天气数据,具体的请求方法示例为:
左右滑动查看全部代码>>>
GET https://api.seniverse.com/v3/weather/now.json?key=2owqvhhd2dd9o9f8&location=beijing&language=zh-Hans&unit=c
对应代码如下:
左右滑动查看全部代码>>>
/* 秘钥,注意!!如果要用这一份代码,这个一定要改为自己的,因为这个我已经故意改错了,防止有人与我公用一个KEY */
#define KEY "2owqvhhd2dd9o9f8" // 这是在心知天气注册后,每个用户自己的一个key
/* GET请求包 */
#define GET_REQUEST_PACKAGE \
"GET https://api.seniverse.com/v3/weather/%s.json?key=%s&location=%s&language=zh-Hans&unit=c\r\n\r\n"
/* JSON数据包 */
#define NOW_JSON "now"
#define DAILY_JSON "daily"
//....还用更多其他的天气数据包可查阅心知天气
/* 组合GET请求包 */
sprintf(GetRequestBuf, GET_REQUEST_PACKAGE, weather_json, KEY, location);
/* 发送数据到服务端 */
write(ClientSock, GetRequestBuf, strlen(GetRequestBuf));
这里简单复习一下sprintf函数
的用法:
(1)函数功能:字符串格式化
(2)函数原型:int sprintf(char *string, char *format [,argument,…]);
string:这是指向一个字符数组的指针,该数组存储了 C 字符串。
format :这是字符串,包含了要被写入到字符串 str 的文本。
[argument]...:根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。
(3)使用示例:
sprintf(buf, "%s,%d", str, num);
假如此时str
为"hello"
,num
为5201314
,则此时buf中的内容为:hello,5201314
,需要注意的是buf的容量要足够大。
4、天气服务端返回的数据
天气服务端给我们天气客户端返回的数据为JSON
格式数据。我们这个天气客户端只是实现了查询此刻天气(对应的数据包为now.json
)及近三天天气情况(对应的数据包为daily.json
),如要查询其他信息,可模仿我们这里处理now.json
和daily.json
的方法,我们用cJson库
进行解析。
这个cJson
库的下载链接为:
链接:https://pan.baidu.com/s/1DQynsdlNyIvsVXmf4W5b8Q
提取码:ww4z
只要把cJSON.c
与cJSON.h
放到工程主程序所在目录,然后在主程序中包含头文件JSON.h
即可引入该库。
为了解析now.json
和daily.json
中的有用数据,我们建立如下结构体:
左右滑动查看全部代码>>>
/* 天气数据结构体 */
typedef struct
{
/* 实况天气数据 */
char id[32]; //id
char name[32]; //地名
char country[32]; //国家
char path[32]; //完整地名路径
char timezone[32]; //时区
char timezone_offset[32]; //时差
char text[32]; //天气预报文字
char code[32]; //天气预报代码
char temperature[32]; //气温
char last_update[32]; //最后一次更新的时间
/* 今天、明天、后天天气数据 */
char date[3][32]; //日期
char text_day[3][64]; //白天天气现象文字
char code_day[3][32]; //白天天气现象代码
char code_night[3][64]; //晚间天气现象代码
char high[3][32]; //最高温
char low[3][32]; //最低温
char wind_direction[3][64]; //风向
char wind_speed[3][32]; //风速,单位km/h(当unit=c时)
char wind_scale[3][32]; //风力等级
}Weather;
现在看一下now.json
的内容是怎样的:
(1)now.json
示例及解析:
now.json:
左右滑动查看全部代码>>>
{
"results": [
{
"location": {
"id": "C23NB62W20TF",
"name": "西雅图",
"country": "US",
"path": "西雅图,华盛顿州,美国",
"timezone": "America/Los_Angeles",
"timezone_offset": "-07:00"
},
"now": {
"text": "多云", //天气现象文字
"code": "4", //天气现象代码
"temperature": "14", //温度,单位为c摄氏度或f华氏度
"feels_like": "14", //体感温度,单位为c摄氏度或f华氏度
"pressure": "1018", //气压,单位为mb百帕或in英寸
"humidity": "76", //相对湿度,0~100,单位为百分比
"visibility": "16.09", //能见度,单位为km公里或mi英里
"wind_direction": "西北", //风向文字
"wind_direction_degree": "340", //风向角度,范围0~360,0为正北,90为正东,180为正南,270为正西
"wind_speed": "8.05", //风速,单位为km/h公里每小时或mph英里每小时
"wind_scale": "2", //风力等级,请参考:http://baike.baidu.com/view/465076.htm
"clouds": "90", //云量,单位%,范围0~100,天空被云覆盖的百分比 #目前不支持中国城市#
"dew_point": "-12" //露点温度,请参考:http://baike.baidu.com/view/118348.htm #目前不支持中国城市#
},
"last_update": "2015-09-25T22:45:00-07:00" //数据更新时间(该城市的本地时间)
}
]
}
这里实测了一下,我们普通用户(因为没充钱,哈哈~)申请的now.json
数据中,now对象
中只有如下三个键值对:
左右滑动查看全部代码>>>
"text": "多云", //天气现象文字
"code": "4", //天气现象代码
"temperature": "14", //温度,单位为c摄氏度或f华氏度
now.json的解析函数:
左右滑动查看全部代码>>>
/*******************************************************************************************************
** 函数: cJSON_NowWeatherParse,解析天气实况数据
**------------------------------------------------------------------------------------------------------
** 参数: JSON:天气数据包 result:数据解析的结果
** 返回: void
********************************************************************************************************/
static int cJSON_NowWeatherParse(char *JSON, Weather *result)
{
cJSON *json,*arrayItem,*object,*subobject,*item;
json = cJSON_Parse(JSON); //解析JSON数据包
if(json == NULL) //检测JSON数据包是否存在语法上的错误,返回NULL表示数据包无效
{
printf("Error before: [%s]\n",cJSON_GetErrorPtr()); //打印数据包语法错误的位置
return 1;
}
else
{
if((arrayItem = cJSON_GetObjectItem(json,"results")) != NULL); //匹配字符串"results",获取数组内容
{
int size = cJSON_GetArraySize(arrayItem); //获取数组中对象个数
#if DEBUG
printf("cJSON_GetArraySize: size=%d\n",size);
#endif
if((object = cJSON_GetArrayItem(arrayItem,0)) != NULL)//获取父对象内容
{
/* 匹配子对象1:城市地区相关 */
if((subobject = cJSON_GetObjectItem(object,"location")) != NULL)
{
// 匹配id
if((item = cJSON_GetObjectItem(subobject,"id")) != NULL)
{
memcpy(result->id, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配城市名
if((item = cJSON_GetObjectItem(subobject,"name")) != NULL)
{
memcpy(result->name, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配城市所在的国家
if((item = cJSON_GetObjectItem(subobject,"country")) != NULL)
{
memcpy(result->country, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配完整地名路径
if((item = cJSON_GetObjectItem(subobject,"path")) != NULL)
{
memcpy(result->path, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配时区
if((item = cJSON_GetObjectItem(subobject,"timezone")) != NULL)
{
memcpy(result->timezone, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配时差
if((item = cJSON_GetObjectItem(subobject,"timezone_offset")) != NULL)
{
memcpy(result->timezone_offset, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
}
/* 匹配子对象2:今天的天气情况 */
if((subobject = cJSON_GetObjectItem(object,"now")) != NULL)
{
// 匹配天气现象文字
if((item = cJSON_GetObjectItem(subobject,"text")) != NULL)
{
memcpy(result->text, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配天气现象代码
if((item = cJSON_GetObjectItem(subobject,"code")) != NULL)
{
memcpy(result->code, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
// 匹配气温
if((item = cJSON_GetObjectItem(subobject,"temperature")) != NULL)
{
memcpy(result->temperature, item->valuestring,strlen(item->valuestring)); // 保存数据供外部调用
}
}
/* 匹配子对象3:数据更新时间(该城市的本地时间) */
if((subobject = cJSON_GetObjectItem(object,"last_update")) != NULL)
{
memcpy(result->last_update, subobject->valuestring,strlen(subobject->valuestring)); // 保存数据供外部调用
}
}
}
}
cJSON_Delete(json); //释放cJSON_Parse()分配出来的内存空间
return 0;
}
5、获取天气数据并解析
实现的总体框图(这是Window版的框图,Linux版的也是类似这样的思路):
6、编译、运行
左右滑动查看全部代码>>>
gcc -std=c99 weather_client.c cJSON.c -o weather_client