《朱老师物联网大讲堂》学习笔记
学习地址:www.zhulaoshi.org
(1).
linux网络编程框架,
网络是分层的,OSI是7层的,这种分层是理论的,
实际应用只有4层,TCP/IP,
处理问题时,一定要知道你自己在哪一层,我们目前关注的是应用层,
因为网络是目前最复杂的通信体系,
曾经有很多类似tcp/ip的协议,
CS,client server,客户端服务器架构,
比如qq客户端与qq服务器进行通信,
BS,broswer server,浏览器服务器架构,
可以把浏览器理解为一个通用的客户端,主流的,
(2).
TCP协议传输特性,
工作在传输层,
对上为socket接口提供服务,对下调用ip层,
面向连接,通信前要先进行3次握手建立连接关系,
tcp好比顺丰,传输可靠,
tcp如何保证可靠传输,
接收方ack给发送方,若发送方未收到ack会重传,
校验码,确保数据未损坏,
滑动窗口技术,来调节网络适配速率,
给报文编号,若接收方收到的编号错误,就会重传,
(3).
建立连接的条件;服务器listen时客户端主动发起connect,
TCP的三次握手,双方之间进行3次单向通信,
SYN,客户端connect发送SYN,进入SYN-SEND状态,
SYN+ACK,服务器收到SYN后,服务器端发送SYN-ACK,进入SYN-RCVD状态,
ACK,客户端进入establishd,发送ACK,服务器收到ACK后,服务器进入establisted,
上面这几步骤,没涉及错误的情况,
关闭连接需要4次挥手,
FIN,客户端FIN,
ACK,服务器ACK,
FIN,服务器FIN,
ACK,客户端ACK,
上面是客户端主动关闭,也可以服务器主动关闭,
上面这些握手,挥手都封装在TCP协议内部,和socket没关系,
基于TCP通信的服务模式,
具有公网ip地址的服务器,公网ip地址有限,不是每个人都能有一个,那么可以使用动态公网ip地址映射技术,
服务器端socket,blind,listen后处于监听状态,
客户端端socket后,直接connect去发起连接,
然后双方就可以建立tcp连接收发数据然后关闭连接,
使用tcp协议的应用,
http,ftp,qq服务器,mail服务器,
(4)
socket编程接口,
建立连接,socket,bind,listen/connect,
int socket(int domain, int type, int protocol);
domain,网络域,ipv4或者其它类型,
type,SOCK_STREAM指的是tcp连接,SOCK_DGRAM指的是udp连接,
protocol,给0使用默认协议,
返回值int,返回一个套接字,有点像文件描述符,
int bind( int socket, const struct sockaddr *address, socklen_t address_len);
把本地ip地址和我们的socket绑定起来,
socket是上一步得到的,address不区分ipv4和ipv6,address_len表示结构体的长度,
int listen(int socket, int backlog);
socket第一,二步的那个,
backlog指定同时监听几个,服务器会有个监听队列,
int connect(int socket, const struct sockaddr *address, socklen_t address_len);
address是要连接的服务器的ip地址,socket是之前打开的,
发送和接收,
ssize_t write( int fildes, const void *buf, size_t nbyte);
fildes其实就是socket,
ssize_t send( int socket, const void *buffer, size_t length, int flags );
flag,正常通信用不到,设为0,此时和write差不多,
ssize_t recv(....);
ssize_t read(....);
ip地址十进制形式,点分二进制形式,下面的函数就是负责两种形式的转换,
inet_aton,inet_ntoa,inet_addr,
inet_ntop,inet_pton,
n代表网络net那端使用的二进制形式,p代表字符串也就是255.255.255.255这种形式,
上面3个不支持ipv6,功能差不多,
ip地址相关数据结构都在netinet/in.h,
struct sockaddr,用来表示一个ip地址,兼容ipv6,这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体,bind、connect等函数中都需要这个结构体,这个结构体是兼容IPV4和IPV6的。在实际编程中这个结构体会被一个struct sockaddr_in或者一个struct sockaddr_in6所填充。
typedef uint32_t in_addr_t; 网络内部用来表示IP地址的类型,
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
(5)
in_addr_t inet_addr( const char *cp );
把点分十进制格式转换为二进制格式的ip地址,这个函数同时会转换为大端模式,
cp = 192.168.1.102,得到0x6601a8c0,数值正确,顺序不同,也就是大小端!
那怎么办呢?
于是统一规定了一个网络字节序,其实大端模式,
大端可以这样记忆,易于机器读出,因为数据肯定是从低地址开始读(比如0地址),而数字等是从高位开始辨识(千位肯定比各位先读出来),
而小端可以这样记忆,想当然得以为,高位数据放高地址,
int inet_pton(int AF, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
(6).
端口号,区分进程,实质是一个数字编号,会包含在每一个发送的数据包中,
bind就是把当前的ip地址和端口号和socket绑定在一起,
int sockfd = 0;
struct sockaddr_in seraddr
sockfd = socket();
htonl,
htons,
h = host,n = net,
l代表4个字节,s代表两个字节
写一部分调试一部分,
(7),(8),(9)
要说的东西主要体现在代码里面,
以下代码为朱老师纯手工打造,
client.c
#include <stdio.h> #include <sys/socket.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #define SERADDR "192.168.1.141" // 服务器开放给我们的IP地址和端口号 #define SERPORT 9003 char sendbuf[100]; char recvbuf[100]; #define CMD_REGISTER 1001 // 注册学生信息 #define CMD_CHECK 1002 // 检验学生信息 #define CMD_GETINFO 1003 // 获取学生信息 #define STAT_OK 30 // 回复ok #define STAT_ERR 31 // 回复出错了 typedef struct commu { char name[20]; // 学生姓名 int age; // 学生年龄 int cmd; // 命令码 int stat; // 状态信息,用来回复 }info; int main(void) { // 第1步:先socket打开文件描述符 int sockfd = -1, ret = -1; struct sockaddr_in seraddr = {0}; struct sockaddr_in cliaddr = {0}; // 第1步:socket sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { perror("socket"); return -1; } printf("socketfd = %d.\n", sockfd); // 第2步:connect链接服务器 seraddr.sin_family = AF_INET; // 设置地址族为IPv4 seraddr.sin_port = htons(SERPORT); // 设置地址的端口号信息 seraddr.sin_addr.s_addr = inet_addr(SERADDR); // 设置IP地址 ret = connect(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr)); if (ret < 0) { perror("listen"); return -1; } printf("成功建立连接\n"); /* while (1) { // 回合中第1步:客户端给服务器发送信息 printf("请输入要发送的内容\n"); scanf("%s", sendbuf); //printf("刚才输入的是:%s\n", sendbuf); ret = send(sockfd, sendbuf, strlen(sendbuf), 0); printf("发送了%d个字符\n", ret); // 回合中第2步:客户端接收服务器的回复 memset(recvbuf, 0, sizeof(recvbuf)); ret = recv(sockfd, recvbuf, sizeof(recvbuf), 0); //printf("成功接收了%d个字节\n", ret); printf("client发送过来的内容是:%s\n", recvbuf); // 回合中第3步:客户端解析服务器的回复,再做下一步定夺 } */ while (1) { // 回合中第1步:客户端给服务器发送信息 info st1; printf("请输入学生姓名\n"); scanf("%s", st1.name); printf("请输入学生年龄"); scanf("%d", &st1.age); st1.cmd = CMD_REGISTER; //printf("刚才输入的是:%s\n", sendbuf); ret = send(sockfd, &st1, sizeof(info), 0); printf("发送了1个学生信息\n"); // 回合中第2步:客户端接收服务器的回复 memset(&st1, 0, sizeof(st1)); ret = recv(sockfd, &st1, sizeof(st1), 0); // 回合中第3步:客户端解析服务器的回复,再做下一步定夺 if (st1.stat == STAT_OK) { printf("注册学生信息成功\n"); } else { printf("注册学生信息失败\n"); } } return 0; }
server.c
#include <stdio.h> #include <sys/socket.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #define SERPORT 9003 #define SERADDR "192.168.1.141" // ifconfig看到的 #define BACKLOG 100 char recvbuf[100]; #define CMD_REGISTER 1001 // 注册学生信息 #define CMD_CHECK 1002 // 检验学生信息 #define CMD_GETINFO 1003 // 获取学生信息 #define STAT_OK 30 // 回复ok #define STAT_ERR 31 // 回复出错了 typedef struct commu { char name[20]; // 学生姓名 int age; // 学生年龄 int cmd; // 命令码 int stat; // 状态信息,用来回复 }info; int main(void) { // 第1步:先socket打开文件描述符 int sockfd = -1, ret = -1, clifd = -1; socklen_t len = 0; struct sockaddr_in seraddr = {0}; struct sockaddr_in cliaddr = {0}; char ipbuf[30] = {0}; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { perror("socket"); return -1; } printf("socketfd = %d.\n", sockfd); // 第2步:bind绑定sockefd和当前电脑的ip地址&端口号 seraddr.sin_family = AF_INET; // 设置地址族为IPv4 seraddr.sin_port = htons(SERPORT); // 设置地址的端口号信息 seraddr.sin_addr.s_addr = inet_addr(SERADDR); // 设置IP地址 ret = bind(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr)); if (ret < 0) { perror("bind"); return -1; } printf("bind success.\n"); // 第三步:listen监听端口 ret = listen(sockfd, BACKLOG); // 阻塞等待客户端来连接服务器 if (ret < 0) { perror("listen"); return -1; } // 第四步:accept阻塞等待客户端接入 clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len); printf("连接已经建立,client fd = %d.\n", clifd); // 客户端反复给服务器发 while (1) { info st; // 回合中第1步:服务器收 ret = recv(clifd, &st, sizeof(info), 0); // 回合中第2步:服务器解析客户端数据包,然后干活, if (st.cmd == CMD_REGISTER) { printf("用户要注册学生信息\n"); printf("学生姓名:%s,学生年龄:%d\n", st.name, st.age); // 在这里服务器要进行真正的注册动作,一般是插入数据库一条信息 // 回合中第3步:回复客户端 st.stat = STAT_OK; ret = send(clifd, &st, sizeof(info), 0); } if (st.cmd == CMD_CHECK) { } if (st.cmd == CMD_GETINFO) { } } return 0; }
http,ftp这些应用层协议,就是这样来的,不过它们设计的很完善。