注:本人已购买韦东山老师第三期项目视频,内容来源《数码相框项目视频》,只用于学习记录,如有侵权,请联系删除。
在此之前我们都是使用 printf 串口打印调试信息的,这存在明显的缺点:① 比较麻烦:程序是运行在设备(这里是开发板)上的,假如我们有很多设备需要同时测试,需要接很多的串口线;② 不好管理;③ 速度慢:假如打印的信息很多,这样打印的速度会变得非常慢;④ 一般,在产品发布的时候,会把相应的串口打印功能去掉,速度和效率上的问题会导致系统在测试时和发布时性能不一致,有些问题就会因此被掩盖。
网络通信实际上也就是数据的传输,在数据传输中需要时刻数据传输的三要素:源、目的、长度。例如,下图有 A、B 两部电脑,网络通信实际上就是A、B两台电脑通信。网络通信有一个特殊的地方,参与数据传输的两个东西,它分为服务器和客户端。其中被动响应的一端称为服务器,主动发起数据传输的一端称为客户端。 下图我们把 A 电脑作为服务器,B电脑作为客户端。
怎么写程序?(记住数传输的三要素:源、目的、长度)
(1) 普通文件的读写:
- ①
fd = open("文件名", ...);
- ②
read(fd, buf, len);
其中,源:fd;目的:buf;长度:len;功能:将 fd 读到 buf 中,读多长?读 len。 - ③
write(fd, buf, len);
其中,源:buf;目的:fd;长度:len;功能:将 buf 写到 fd 中,写多长?写 len。
(2) 网络编程:(服务器:先以 TCP 传输为例)
-
①
fd = socket(..., ..., ...)
,通过 man 手册查看 socket 函数,如下:#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol);
其中参数:
- domain:指明通信域,决定通信使用的网络协议族,协议族不同,地址结构也不同。domain 可以取一下值:(更过的值可查看 man 手册)
- AF_UNIX:UNIX通信域,即同一台计算机内两个进程通过文件系统进行通信要求文件系统的路径名作为套接字的地址;
- AF_INET:网络通信,使用IPv4;要求32位IPv4地址;
- AF_INET6:网络通信,使用IPv6。
- type:指明套接字的类型,套接字的类型指明通信的语义;它可以取以下值:(更过的值可查看 man 手册)
- SOCK_STREAM:TCP 字节流套接字;
- SOCK_DGRAM:UDP 数据报套接字;
- protocol:domain 参数确定套接字采用什么协议族,而此参数在给定的协议族内选择一种具体的协议。对于给定的通信域内的每一种套接字类型,一般只存在 一种协议,在这种情况之下,只要指明了通信域和通信类型,协议就是唯一的, 一般都将此参数置为0,即让系统选择默认的协议。
从 socket 函数的参数可以看出,并不像文件读写一样有数据三要素。那怎么办?那么接下来一定要有数据三要素的相关操作。
- domain:指明通信域,决定通信使用的网络协议族,协议族不同,地址结构也不同。domain 可以取一下值:(更过的值可查看 man 手册)
-
② 服务器是怎么知道客户端发来消息的呢?肯定是要循环监测网卡上的某个端口,当客户端发来请求时,和客户端建立联系。所以我们需要一个函数将网卡上的 ip 和端口绑定起来,从而形成一个目的地址,这个函数就是
bind(..., ..., ...)
, 用于绑定 fd 和 服务器自己的 ip 端口,即把 fd 和 ip 端口建立联系。 man 手册中,bind 函数代码如下:#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其中参数:
-
sockfd: 通过 socket 函数得到的文件描述符;
-
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址,sockaddr 结构体的代码如下:
struct sockaddr { sa_family_t sa_family; char sa_data[14]; }
-
addrlen:对应地址的长度。
相对来说,源就是客户端发来的请求了,长度我们可以用 sizeof 求出来。
-
-
③ 回到上面的问题,我们需要不断检测某个端口,所以首先应该用一个函数来监测数据,这个函数就是
listen(..., ...);
listen 表示开始监测数据,即启动监测数据。#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog);
其中参数:
- sockfd: 通过 socket 函数得到的文件描述符;
- backlog:未经过处理的连接请求队列可以容纳的最大数目。
-
④ 启动监测之后,我们还需要和客户端建立联系,该函数是
accept(..., ..., ...)
,用于接受客户端的连接,代码如下:#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
⑤ 当客户端发出请求后, accept 用于建立客户端和服务器端的联系,之后就可以传输数据了:
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); /* 发送数据 */ ssize_t recv(int sockfd, void *buf, size_t len, int flags); /* 接收数据 */
一般的,我们都是循环发送和接收数据的,所以,它们两个在使用的时候,一般用一个 while(1)循环发送和接收数据。
(3) 网络编程:(客户端:以 TCP 传输为例)
- ①
fd = socket(..., ..., ...)
; - ②
connect()
用于主动向服务器发起建立连接; - ③ 建立连接之后,可以进行数据的传输:
ssize_t send(int sockfd, const void *buf, size_t len, int flags); /* 发送数据 */ ssize_t recv(int sockfd, void *buf, size_t len, int flags); /* 接收数据 */
(4) 网络编程:(服务器:以 UDP 传输为例)
- ①
fd = socket(..., ..., ...)
; - ②
bind(fd, ..., ...)
绑定 IP 和 端口等信息; - ③
recvfrom()
接受数据;sendto()
发送数据。
(5) 网络编程:(客户端:以 UDP 传输为例)
- ①
fd = socket(..., ..., ...)
; - ②
connect()
用于主动向服务器发起建立连接,这个和 tcp 的连接包含三次握手,只是用于设置服务器的 IP 地址和端口等信息; - ③ 使用
send()
或sendto()
发送数据;使用recv()
或recvfrom()
接收数据;
代码编写:(实现的功能:客户端向服务器发送信息(TCP))
(1) 编写服务器程序 server.c:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
/* socket
* bind
* listen
* accept
* send/recv
*/
#define SERVER_PORT 8888
#define BACKLOG 10
/* 网络编程可阅读三期视频提供的文档《Linux网络编程入门》 */
int main(int argc, char **argv)
{
int iSocketServer;
int iSocketClient;
int iRet;
int iAddrLen;
struct sockaddr_in tSocketServerAddr;
struct sockaddr_in tSocketClientAddr;
unsigned char ucRecvBuf[1000];
int iRecvLen;
int iClientNum = -1;
signal(SIGCHLD, SIG_IGN); /* 避免子进程退出后,变为僵死进程,关于僵死进程可阅读三期视频提供的文档《僵死进程》 */
iSocketServer = socket(AF_INET, SOCK_STREAM, 0);
if (iSocketServer == -1)
{
printf("socket error\n");
return -1;
}
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY; /* INADDR_ANY表示可以和任何的主机通信 */
memset((unsigned char*)tSocketServerAddr.sin_zero, 0, sizeof(tSocketServerAddr.sin_zero));
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(tSocketServerAddr));
if (iRet == -1)
{
printf("bind error\n");
return -1;
}
iRet = listen(iSocketServer, BACKLOG);
if (iRet == -1)
{
printf("listen error\n");
return -1;
}
while (1)
{
/* 等待,接受连接 */
iAddrLen = sizeof(struct sockaddr);
iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
if (iSocketClient != -1)
{
iClientNum++;
printf("Get Connect from client %d: %s\n", iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));
/* 每接受到一个连接,创建一个进程 */
if (!fork())
{
/* 子进程的源码 */
while (1)
{
/* 接收客户端发来的数据,并显示出来 */
iRecvLen = recv(iSocketClient, ucRecvBuf, 999, 0);
if (iRecvLen <= 0)
{
close(iSocketClient);
return -1;
}
else
{
ucRecvBuf[iRecvLen] = '\0';
printf("Get Msg From Client %d: %s\n", iClientNum, ucRecvBuf);
}
}
}
}
}
close(iSocketServer);
return 0;
}
(2) 编写客户端程序 client.c:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
/* socket
* connect
* send/recv
*/
#define SERVER_PORT 8888
#define BACKLOG 10
int main(int argc, char **argv)
{
int iSocketClient;
int iRet;
struct sockaddr_in tSocketServerAddr;
unsigned char ucSendBuf[1000];
int iSendLen;
if (argc != 2)
{
printf("Usage:\n");
printf("%s <server_ip>\n", argv[0]);
return -1;
}
iSocketClient = socket(AF_INET, SOCK_STREAM, 0);
if (iSocketClient == -1)
{
printf("socket error\n");
return -1;
}
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr)) /* 把字符串IP地址转换为 sin_addr 结构体 */
{
printf("invalid server_ip\n");
return -1;
}
memset((unsigned char*)tSocketServerAddr.sin_zero, 0, sizeof(tSocketServerAddr.sin_zero));
iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (iRet == -1)
{
printf("connect error\n");
return -1;
}
while (1)
{
if (fgets(ucSendBuf, 999, stdin)) /* 输入回车后, fgets 返回 */
{
iSendLen = send(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0);
if (iSendLen <= 0)
{
close(iSocketClient);
return -1;x
}
}
}
return 0;
}
(3) 测试:
- ① 首先运行服务器程序:
./server
; - ② 运行客户端程序:
./client 192.168.0.103
(注:192.168.0.103 为服务器 ip 地址),此时服务器会显示已经连接上客户端,如下图所示:(注:因为服务器程序和客户端程序都是运行在同一台PC上,所以服务器IP地址与客户端IP地址是一样的)
- ③ 再运行一个客户端程序:
./client 127.0.0.1
(注:127.0.0.1 是本机回环地址),此时服务器端继续弹出Get Connect from client 1: 127.0.0.1
,如下图所示:
- ④ 两个客户端向服务器发送消息,在客户端 0 输入
abc
,然后输入回车;,在客户端 0 输入123456
,然后输入回车;服务器收到的信息如下:
- ⑤ 在终端输入命令
ps -A
查看进程,如下图所示:可看到有3个server进程(两个子进程,一个父进程)和2个client进程,这与 server 程序每接受一个连接就创建一个进程相符。
- ⑥ 当我们停止运行一个客户端时,再一次在终端输入命令
ps -A
查看进程,如下图所示:可见只剩下了两个 server 进程和一个 client 进程。
注:在 server.c 程序中,需要添加signal(SIGCHLD, SIG_IGN);
在子进程结束的时候清理子进程。否则,会产生一个僵死进程,如下图所示:
代码编写:(实现的功能:客户端向服务器发送信息(UDP))
(1) 编写服务器程序 server.c:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
/* socket
* bind
* recvfrom/sendto
*/
#define SERVER_PORT 8888
int main(int argc, char **argv)
{
int iSocketServer;
int iRet;
struct sockaddr_in tSocketServerAddr;
struct sockaddr_in tSocketClientAddr;
int iRecvLen;
unsigned char ucRecvBuf[1000];
int iAddrLen;
iSocketServer = socket(AF_INET, SOCK_DGRAM, 0); /* SOCK_DGRAM:UDP数据报 */
if (iSocketServer == -1)
{
printf("socket error\n");
return -1;
}
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY; /* INADDR_ANY表示可以和任何的主机通信 */
memset((unsigned char*)tSocketServerAddr.sin_zero, 0, sizeof(tSocketServerAddr.sin_zero));
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (iRet == -1)
{
printf("bind error\n");
return -1;
}
while (1)
{
iAddrLen = sizeof(struct sockaddr);
iRecvLen = recvfrom(iSocketServer, ucRecvBuf, 999, 0, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
if (iRecvLen > 0)
{
ucRecvBuf[iRecvLen] = '\0';
printf("Get Msg From %s: %s\n", inet_ntoa(tSocketClientAddr.sin_addr), ucRecvBuf);
}
}
close(iSocketServer);
return 0;
}
(2) 编写客户端程序 client.c:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
/* socket
* connect
* send/sendto/recv/recvfrom
*/
#define SERVER_PORT 8888
#define BACKLOG 10
int main(int argc, char **argv)
{
int iSocketClient;
int iRet;
struct sockaddr_in tSocketServerAddr;
unsigned char ucSendBuf[1000];
int iSendLen;
if (argc != 2)
{
printf("Usage:\n");
printf("%s <server_ip>\n", argv[0]);
return -1;
}
iSocketClient = socket(AF_INET, SOCK_DGRAM, 0); /* SOCK_DGRAM:UDP数据报 */
if (iSocketClient == -1)
{
printf("socket error\n");
return -1;
}
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr)) /* 把字符串IP地址转换为 sin_addr 结构体 */
{
printf("invalid server_ip\n");
return -1;
}
memset((unsigned char*)tSocketServerAddr.sin_zero, 0, sizeof(tSocketServerAddr.sin_zero));
/* 在udp传输中,实际上connect并不会真正建立连接,只是用于设置端口ip等信息 */
iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (iRet == -1)
{
printf("connect error\n");
return -1;
}
while (1)
{
if (fgets(ucSendBuf, 999, stdin)) /* 输入回车后, fgets 返回 */
{
iSendLen = send(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0);
if (iSendLen <= 0)
{
close(iSocketClient);
return -1;
}
}
}
return 0;
}
注:在 client.c 中,假如数据发送函数使用 sendto()
,就可以不使用 connect()
函数了,因为 sendto()
函数的参数包含了目的地址, sendto()
原型如下:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
(3) 测试:
- ① 首先运行服务器程序:
./server
; - ② 运行客户端程序:
./client 192.168.0.103
,并输入 abcd,然后输入回车,服务器 server 收到的信息如下图所示:
- ③ 运行客户端程序:
./client 127.0.0.1
,并输入 9876543210 ,然后输入回车,服务器 server 收到的信息如下图所示:
可见测试是成功的。