一、学习内容
-
ip地址的网络字节序转换
-
函数原型
in_addr_t inet_addr(const char *cp);-
返回值
in_addr_t:一个 uint32 数据,该数据是结构体struct in_addr {in_addr_t s_addr;};struct in_addr 是结构体 struct sockaddr_in 中的一个数据
-
参数描述
参数 cp:ip地址 -
调用形式
struct sockaddr_in addr
addr.sin_addr.s_addr = inet_addr("192.168.1.1") -
功能描述
将本地字节序的ip地址转换成网络字节序
-
-
-
套接字
-
概念
专门用来进行网络通信的一种文件:该文件中保存了数据接收端的ip地址和port端口号 -
创建一个套接字
-
函数原型
int socket(int domain, int type, int protocol);-
参数描述
-
参数 domain:网络介质
AF_UNIX, AF_LOCAL:本地通信用的套接字 AF_INET :ipv4协议 -
参数 type:套接字类型
SOCK_STREAM:提供一个基于连接的,稳定,双向的,字节流的套接字,TCP协议就是这种套接字
SOCK_DGRAM:提供一个非连接,不可靠的,要求数据有最大值限制的 数据报套接字,UDP协议就是这种套接字 -
参数 protocol:协议
写 0:表示根据套接字类型自动选择协议
-
-
调用形式
int sock = socket(AF_INET,SOCK_STREAM,0) 字节流套接字
int sock = socket(AF_INET,SOCK_DGRAM,0) 数据报套接字 -
功能描述
创建一个套接字文件
-
-
-
-
服务器模型
-
创建一个字节流的套接字
-
准备一个 互联网地址结构体,用来存放ip和port
互联网地址结构体,tcp使用的结构体类型如下 struct sockaddr_in {__kernel_sa_family_t sin_family; 必须写 AF_INET__be16 sin_port; 网络字节序的端口号struct in_addr sin_addr; 网络字节序的ip,注意是该变量里面一个叫做 s_addr的变量存放网络字节序的ip unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -sizeof(unsigned short int) - sizeof(struct in_addr)]; };最后一个变量:唯一作用就是让 struct sockaddr_in 这个结构体的大小去和 struct sockaddr 这个结构体大小对齐 为什么要对齐:因为在 下一步绑定里面,需要用到的是 sockaddr 类型 所以问题又变成了,下一步绑定的时候,为什么要用sockaddr,而不是 sockaddr_in呢: 因为刚才说过,根据套接字的类型不同, 使用的地址信息结构体可能是不同的(tcp udp使用的是 sockaddr_in,域套接字使用的是 sockaddr_un), 所以需要一个通用的地址信息结构体,去接受不同类型的地址信息结构体里面的数据, 为了能够顺利转换数据,这些不同类型的地址信息结构体,大小必须和通用地址信息结构体保持一致
-
将ip和port绑定到套接字里面
-
函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);-
参数描述
-
参数 sockfd:等待绑定ip和port的套接字
-
参数 addr:包含有ip和port的地址信息结构体的地址,详见上一步中的解释
-
参数 addrlen:参数addr的实际长度
-
-
调用形式
由于存在端口占用的问题,bind函数非常容易出错, 所以需要判断他的返回值,bind失败返回-1struct sockaddr_in addr;if(bind(套接字,(struct sockaddr*)&addr,sizeof(addr)) == -1){perror("bind") }
-
功能描述
为套接字sockfd,绑定ip地址和端口号port
-
-
-
服务器创建一个监听列表
-
函数原型
int listen(int sockfd, int backlog);-
解释
如果监听列表长度为10,然后此时有11个客户端尝试连接,但是这11个客户端的前10个,都没有被服务器接受连接,此时监听列表就会满,第11个客户端就会排队。直到服务器接受一个客户端的连接,让监听列表空出一个位置为止 -
参数描述
-
参数 sockfd:谁去监听连接,一般就是服务器
-
参数 backlog:监听列表的长度
-
-
调用形式
-
listen(server,10)
-
-
功能描述
创建监听列表,并监听是否有客户端连接
-
-
-
服务器接受客户端的连接
-
函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);-
参数描述
-
参数 sockfd:服务器描述符
-
参数 addr:传入一个 struct sockaddr_in 结构体地址,用来存放连接上来的客户端的ip和port
如果传0,表示不需要知道客户端的 ip 和 port -
参数 addrlen:注意是一个指针,该指针指向参数2所指向的那个结构体的实际长度,我们需要提前准备一下
-
-
调用形式
-
最简单的调用形式
int client = accept(server,NULL,NULL) -
复杂一点的形式
struct sockaddr_in client_addr = {0};socklen_t client_len = sizeof(client_addr);int client = accept(server,(struct sockaddr*)&client_addr,&client_len)
-
-
功能描述
该函数是一个阻塞型IO,如果没有客户端来连接的话,那么accept将会一直阻塞,直到有客户端连接为止,accept函数将会接受客户端的连接,然后获取客户端的ip和port,并且返回客户端的描述符
-
为什么要返回客户端描述符
因为服务器与客户端产生连接之后,服务器与客户端之间的通信,全都依赖客户端套接字 -
为什么不依赖服务器套接字通信呢
因为一个服务器允许连接多个客户端,如果使用服务器套接字通信的话,根本不知道会向哪一个客户端进行通信 -
服务器套接字功能
仅仅是展示自己的ip和port,让客户端来连接
-
-
-
-
模型总结
-
-
客户端模型
-
创建套接字:socket
-
准备互联网地址信息结构体,并填充信息
-
连接服务器
-
函数原型
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);-
调用形式
直接照抄bind函数的调用形式,再把函数名改成connect就行了 -
功能描述
根据地址信息结构体中的ip和port,连接服务器 由于服务器可能没开,所以connect函数的错误率也很高,所以记得判断他的返回值,返回-1表示链接失败
-
-
-
模型总结
-
-
客户端与服务器收发消息
-
发送消息
由于套接字本身也是一个描述,使用write发送消息没有任何问题。还有一个专门针对套接字进行消息发送的函数-
函数原型
ssize_t send(int sockfd, const void *buf, size_t len, int flags);-
参数描述
-
参数 sockfd:套接字
-
参数 buf:存放有准备发送的数据的地址
-
参数 len:数据的实际长度
-
参数 flags:写0的时候,send和write一模一样,表示阻塞型IO,还有一个选项 MSG_DONTWAIT:表示非阻塞型IO,当缓存区写满之后,新写入的数据丢失
-
-
调用形式
send(套接字,准备发送的数据的地址,数据的长度,0) -
功能描述
write是unix系统中的一个库函数
send是 posix标准中的一个标准库函数
send其实前3个参数和write的用法一模一样,区别仅在于最后一个flags
-
-
-
读取消息
同理,使用read读取消息也没有任何问题 同理,存在一个专门用于套接字的读取函数-
函数原型
ssize_t recv(int sockfd, void *buf, size_t len, int flags);-
注意
recv和read对比,唯一的优势就是 recv如果想要切换成非阻塞IO很简单,read如果想要切换成非阻塞IO稍微复杂一点 -
参数描述
-
参数 sockfd:套接字
-
参数 buf:存放有准备发送的数据的地址
-
参数 len:数据的实际长度
-
参数 flags:写0的时候,recv和read一模一样,表示阻塞型IO,还有一个选项 MSG_DONTWAIT:表示非阻塞型IO,当缓存区写满之后,新写入的数据丢失
-
-
调用形式
recv(套接字,准备发送的数据的地址,数据的长度,0) -
功能描述
接受套接字中的数据
-
-
-
read函数如何切换成非阻塞型IO
-
函数原型
int fcntl(int fd, int cmd, ... /* arg */ );-
参数描述
-
参数 fd:等待操作的描述符
-
参数 cmd:决定了 fcntl函数到底是设置flags 还是 获取 flags
F_GETFL:获取flags的值
F_SETFL:设置flags的值 -
参数 ...:根据 cmd的值,决定是否需要传入第3个参数
如果cmd表示获取flags的值,则不需要传入第3个参数
如果cmd表示设置flags的值,则需要传入第3个参数,第3个参数传入想要设置的flags的具体的值
-
-
返回值
获取flags,成功获取返回flags,失败获取返回-1
设置flags,成功返回0,失败返回-1 -
功能描述
用来设置一个描述符的flags(open函数的第2个参数,O_RDONLY这种东西) 或者用来获取一个描述符的flags
-
-
-
read和recv的特点
当read 和 recv 是一个阻塞型IO的时候,此时如果客户端断开,则服务器那边的客户端套接字的read就会变成一个非阻塞型IO
当read变成非阻塞型IO之后,由于没有读取到数据,会返回0
反过来说,read函数如果从阻塞型IO变成非阻塞型IO(说明客户端断开连接),我们只需要判断返回值是否为0即可
但是,当read从一开始就是一个非阻塞型IO呢? 没有读取到数据,返回-1,如果客户端断开连接,返回0
-
-
脑图
二、作业
作业
使用搭建好的服务器和客户端,实现一个完整的注册,登录功能
服务器使用链表 + 文件IO的形式去记录账号和密码
代码解答:
服务器:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <semaphore.h>
#include <wait.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <sys/msg.h>
#include <sys/shm.h>
#include <sys/un.h>// 定义简化的套接字地址类型
typedef struct sockaddr_in addr_in_t;
typedef struct sockaddr addr_t;
typedef struct sockaddr_un addr_un_t;// 定义用户数据结构
struct Pack {char name[16]; // 用户名char pswd[16]; // 密码
};// 定义链表节点结构
typedef struct Iceberg {union {struct Pack userdata; // 用于存储用户数据int len; // 用于存储链表长度};struct Iceberg *next; // 下一个节点指针
} liink, *Plink;// 创建链表头节点
Plink create() {Plink p = malloc(sizeof(liink));if (p == NULL) {printf("申请头节点失败\n");return NULL;}p->len = 0;p->next = NULL;return p;
}// 从user.txt文件读取用户信息并加载到链表中
void read_usertxt(Plink L) {if (L == NULL) {printf("载入失败\n");return;}int fd = -1;if ((fd = open("./user.txt", O_RDWR)) == -1) {perror("open error");return;}struct Pack pack;Plink t = L;for (int i = 0; i < L->len; i++) {t = t->next;}while (1) {int res = read(fd, &pack, sizeof(pack));if (res == 0) {printf("原有用户已载入完成\n");break;}Plink p = malloc(sizeof(liink));memcpy(p->userdata.name, pack.name, sizeof(pack.name));memcpy(p->userdata.pswd, pack.pswd, sizeof(pack.pswd));p->next = NULL;t->next = p;L->len++;}close(fd);return;
}// 检查用户名是否存在并进行注册
int Plagiarism_detection_reg(struct Pack pack1, Plink L) {int flag = 0;Plink t = L;for (int i = 0; i < L->len; i++) {t = t->next;if (strcmp(t->userdata.name, pack1.name) == 0) {flag = 1;break;}}if (flag == 1) {printf("用户名已存在\n");return 1;} else if (flag == 0) {Plink p = malloc(sizeof(liink));memcpy(p->userdata.name, pack1.name, sizeof(pack1.name));memcpy(p->userdata.pswd, pack1.pswd, sizeof(pack1.pswd));p->next = NULL;t->next = p;L->len++;printf("用户注册成功\n"); return 0;}
}// 用户登录验证
int Plagiarism_detection_login(struct Pack pack2, Plink L) {int flag = 0; Plink t = L;for (int i = 0; i < L->len; i++) {t = t->next;if ((strcmp(t->userdata.name, pack2.name) == 0) && (strcmp(t->userdata.pswd, pack2.pswd) == 0)) {flag = 1;break;}}if (flag == 1) {printf("登录成功\n");return 1;} else if (flag == 0) {printf("用户名或密码错误\n");return 0;}
}// 将链表中的用户信息保存到user.txt文件
int save_usertext(Plink L) {int fd = -1;if ((fd = open("./user.txt", O_WRONLY | O_TRUNC)) == -1) {perror("open error");return -1;}Plink t = L->next;while (t != NULL) {if (write(fd, t->userdata.name, sizeof(t->userdata.name)) == -1) {perror("write name error");break;}if (write(fd, t->userdata.pswd, sizeof(t->userdata.pswd)) == -1) {perror("write pswd error");break;}t = t->next;}close(fd);return 0;
}// 销毁链表
int link_destory(Plink L) {if (L == NULL) {printf("销毁失败\n");return -1;}Plink t = L;while (t != NULL) {t = t->next;free(L);L = t;}printf("链表销毁完成\n");
}// 主函数
int main(int argc, const char *argv[]) {if (argc != 2) {printf("请输入端口号\n");return -1;}int port = atoi(argv[1]); // 端口号转换为整数int sever = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字addr_in_t addr = {0}; // 初始化地址结构体addr.sin_family = AF_INET; // 设置地址家族addr.sin_port = htons(port); // 设置端口号addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置IP地址if (bind(sever, (addr_t*)&addr, sizeof(addr)) == -1) { // 绑定地址perror("bind error");return -1;}listen(sever, 10); // 监听连接addr_in_t client_addr = {0}; // 初始化客户端地址socklen_t client_len = sizeof(client_addr);int client = accept(sever, (addr_t*)&client_addr, &client_len); // 接受客户端连接printf("客户端连接成功\n");Plink L = create(); // 创建链表头节点read_usertxt(L); // 读取用户信息while (1) {int flag = -1;read(client, &flag, sizeof(flag)); // 读取客户端标志if (flag == 2) { // 注册struct Pack pack1;read(client, &pack1, sizeof(pack1)); // 读取用户信息Plagiarism_detection_reg(pack1, L); // 注册用户save_usertext(L); // 保存用户信息} else if (flag == 1) { // 登录struct Pack pack2;read(client, &pack2, sizeof(pack2)); // 读取登录信息Plagiarism_detection_login(pack2, L); // 验证登录} else { // 退出save_usertext(L); // 保存用户信息link_destory(L); // 销毁链表printf("客户端断开连接\n");close(client); // 关闭客户端连接break;}}return 0;
}
客户端:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <semaphore.h>
#include <wait.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <sys/msg.h>
#include <sys/shm.h>
#include <sys/un.h>// 定义用户信息结构体
struct Pack {char name[16]; // 用户名char pswd[16]; // 密码
};// 主函数
int main(int argc, const char *argv[]) {// 检查命令行参数,要求提供端口号if (argc != 2) { printf("请输入端口号\n");return -1;}int port = atoi(argv[1]); // 将端口号字符串转换为整数int client = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字用于TCP通信// 初始化服务器地址结构体struct sockaddr_in addr = {0}; addr.sin_family = AF_INET; // 设置地址族为IPv4addr.sin_port = htons(port); // 设置端口号,转换为网络字节序addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置服务器IP地址// 连接服务器,若失败则退出if (connect(client, (struct sockaddr*)&addr, sizeof(addr)) == -1) { perror("connect error");return -1;}// 主循环,提供用户交互菜单while (1) {struct Pack pack; // 定义用于存储用户名和密码的结构体变量// 输出操作菜单printf("\t\t\t1、登录\t\t\t\n"); printf("\t\t\t2、注册\t\t\t\n");printf("\t\t\t0、退出\t\t\t\n");int flag = -1; // 用户选项标识符scanf("%d", &flag); // 获取用户选择if (flag == 0) { // 如果用户选择退出write(client, &flag, sizeof(flag)); // 发送退出标志到服务器break; // 结束循环,退出客户端}write(client, &flag, sizeof(flag)); // 将用户选择的标志发送到服务器// 根据用户选择执行相应操作if (flag == 1) { // 用户选择登录printf("请输入用户名\n");scanf("%s", pack.name); // 输入用户名printf("请输入密码\n");scanf("%s", pack.pswd); // 输入密码write(client, &pack, sizeof(pack)); // 将用户名和密码发送到服务器进行验证} else if (flag == 2) { // 用户选择注册printf("请输入用户名\n");scanf("%s", pack.name); // 输入用户名printf("请输入密码\n");scanf("%s", pack.pswd); // 输入密码write(client, &pack, sizeof(pack)); // 将注册信息发送到服务器进行注册} else { // 输入其他无效选项printf("无效选择,请重新输入。\n");}}close(client); // 关闭客户端套接字return 0; // 正常退出
}
成果展现:
三、总结
学习内容概述
今天的学习内容主要涉及网络编程的基础知识,包括 IP 地址的网络字节序转换、套接字(socket)的概念、套接字的创建与绑定、服务器和客户端的通信流程、数据的发送与接收等。重点掌握了套接字的使用流程,学习了各类 socket 相关函数(如 `socket`、`bind`、`listen`、`accept`、`connect`、`send`、`recv` 等)及其作用。
学习难点
1. 网络字节序转换:
理解 IP 地址的网络字节序和本地字节序之间的转换,对于跨网络传输的数据表示尤为重要。
2. 套接字的多样化使用:
不同的套接字类型(如 SOCK_STREAM 和 SOCK_DGRAM)适用于不同的传输协议(TCP 和 UDP),需要理解不同协议的应用场景。
3. 网络编程模型:
在服务器和客户端模型中,对各函数的调用顺序和作用(如 `bind`、`listen`、`accept` 等)需要深刻理解,否则容易导致程序运行异常。
4. 非阻塞 I/O:
理解阻塞和非阻塞 I/O 的区别,以及如何利用 `fcntl` 函数切换 I/O 模式,尤其是在网络编程中正确处理非阻塞 I/O 的返回值,是一个较难掌握的点。
主要事项
1. IP 地址转换:
`inet_addr` 函数可以将本地字节序的 IP 地址转换为网络字节序,以便在套接字通信中传输。常用于将人类可读的 IP 地址转换为适用于网络传输的数据格式。
2. 套接字创建与绑定:
通过 `socket` 函数创建套接字文件,使用 `bind` 函数将 IP 和端口绑定到套接字上。特别注意的是,`bind` 可能会因为端口占用而失败,需要进行错误处理。
3. 服务器模型:
服务器通过 `listen` 函数创建监听列表,并使用 `accept` 函数接收客户端连接。`accept` 返回的客户端描述符用于后续的通信,这样可以区分不同客户端。
4. 数据传输:
`send` 和 `recv` 函数用于套接字的数据发送和接收,和 `write`、`read` 类似,但有更强的网络适应性。对于需要实现非阻塞 I/O 的场景,可以使用 `fcntl` 设置描述符的 flags。
5. I/O 模式切换:
`fcntl` 函数的 `F_SETFL` 选项可以用于切换描述符的阻塞模式,在网络断开时可判断 `read` 或 `recv` 返回值,识别客户端是否断开连接。
未来学习的重点
1. 深入理解套接字参数和结构体:
如 `struct sockaddr` 和 `struct sockaddr_in` 结构体的区别和联系,了解其各字段的具体含义,以及如何在实际编程中进行正确填充和使用。
2. 网络编程中的错误处理:
套接字编程中常出现的错误,如端口占用、连接失败等,后续学习中可以尝试更多的错误处理方法,提升程序的健壮性。
3. 非阻塞 I/O 和多线程/多进程结合:
深入学习非阻塞 I/O 的使用技巧,特别是在高并发场景下如何配合多线程或多进程处理大量客户端请求,提升程序性能。
4. 协议栈的学习:
进一步理解 TCP/IP 协议栈的各层原理,掌握 TCP 和 UDP 的差异及其具体应用场景,以更好地选择适当的套接字类型和传输协议。