目录
前言
周四
一、网络编程概述
1、协议就是指通信双方约定好的数据发送和接受以及解析的规则。
2、网络的发展阶段可分为四个:
二、网络基础知识
1、字节序
2、如何判断自己的主机是大端存储还是小端存储
3、什么情况需要转换字节序,什么情况不需要
4、字节序转换的函数
5、socket函数
6、IP地址
7、端口号
三、TCP/UDP的概念和异同
1、TCP(即传输控制协议)概念:
2、UDP(User Datagram Protocol)概念:
3、TCP/UDP的异同
四、TCP网络编程
1、TCP网络模型
2、TCP网络编程流程
3、函数说明
4、搭建简易循环服务器及代码
周五
一、TCP网络编程注意事项
1、TCP的粘包问题
二、UDP网络编程
1、UDP网络编程流程
2、recvfrom 函数
3、sendto 函数
4、简单UDP服务器模型的实现
三、TFTP协议
总结
连续七天课中的最后两天,开了一门新课:网络编程,课程为时八天,最后一天需要利用到目前为止所有学到的内容,独立完成一个综合项目。
这两天的学习的主要内容有:第一天,网络编程概述、网络基础知识和TCP网络编程;第二天,UDP网络编程。
这两天的重点难点有:开放系统互联模型的结构,TCP/IP协议族体系的结构以及体系中各层次的协议,TCP和UDP的异同,以及TCP和UDP网络编程的使用。
网络编程的课程特点是:框架较为固定,更加注重思维逻辑,代码量较大,本次总结将围绕这两天的重点难点,深化理论知识理解、强化TCP和UDP网络编程的代码逻辑。
1、ARRAnet阶段:
使用的协议为NCP(network control protocol),特点是不能互联不同类型的计算机和不同类型的系统,没有纠错功能。
2、TCP/IP两个协议阶段:
TCP协议:用来检测网络传输中的差错的传输控制协议;
IP协议:专门负责对不同的网络进行互联的互联网协议;
3、网络体系结构和开放系统互联模型
网络体系结构:分层而治之,层次之间相互协同,是网络的层次结构和每层所使用的协议的集合。
开放系统互联模型(OSI):是一个理想化的模型,分为七层,从上到下依次是:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层(物数网传会表应)。
4、TCP/IP协议族体系结构
TCP/IP协议族是可以应用的工业标准,分为四层,从上往下分别为:应用层,传输层,网络层和链路层。
TCP/IP与OSI模型对应关系:
小端字节序(little-endian):低字节存储在地址低位、高字节存储在地址高位;
大端字节序(big-endian):高字节存储在地址地位、低字节存储在地址高位;
主机字节序:不同CPU的主机,可能是小端序,可能是大端序。
网络字节序:规定为大端序,发送数据前,需要将主机字节序转为网络字节序,以保证发送和读取的数据一致。
方法1:使用指针截取
#include
int main(int argc, const char *argv[])
{
int num = 0x12345678;
char *p = (char *)#
if(0x12 == *p){
printf("大端\n");
}else if(0x78 == *p){
printf("小端\n");
}
return 0;
}
方式2:使用共用体
#include
union Test{
int a;
char b;
};
int main(int argc, const char *argv[])
{
union Test t;
t.a = 0x12345678;
if(0x12 == t.b){
printf("大端\n");
}else if(0x78 == t.b){
printf("小端\n");
}
return 0;
}
需要转换:数据是多字节作为一个整体;
不需要转换:已知传输数据的双方主机为相同的字节序,或者传输的字节序是字符串。
h是host,代表主机字节序,n为network,代表网络字节序
l是long,代表4字节,s为short,代表2字节
#include
uint32_t htonl(uint32_t hostlong); //主机转网络 4字节
uint16_t htons(uint16_t hostshort); //主机转网络 2字节
uint32_t ntohl(uint32_t netlong); //网络转主机 4字节
uint16_t ntohs(uint16_t netshort); //网络转主机 2字节
socket函数可用于同主机的进程间通信;
socket函数与TCP/IP协议族配合使用,可实现不同主机的进程间通信;
socket是一个编程接口,是一种特殊的文件描述符,并不仅限于TCP/IP协议,还有UDP协议,它将复杂的网络通信过程封装成IO操作。
套接字的分类:
流式套接字(SOCK_STREAM)--TCP
数据套接字(SOCK_DGRAM)--UDP
原始套接字(SOCK_RAM)
IPV4 4字节 32bit 由网络号和主机号组成
IPV6 16字节 128bit
对于IPV4而言,IP地址的表示形式 "192.168.80.10" 这种叫做点分十进制,是一个字符串
计算机中存储IP地址是用的无符号4字节整型(unsigned int)。就涉及到了字节序问题:
//将点分十进制的字符串 转换成 网络字节序的 无符号四字节整型
in_addr_t inet_addr(const char *cp);
//将网络字节序的无符号四字节整型的ip地址 转换成 点分十进制的字符串
char *inet_ntoa(struct in_addr in);
如果使用pid表示端口号,会有两个问题:一是pid由系统随机分配,无法指定,二是系统重启后,进程号会改变,无法统一管理。利用端口号,人为标识某个进程,可以方便使用和管理;
linux系统端口号的范围是[0-65535],共计6k个,使用unsigned short存储;
实际开发中,为防止指定的端口号与系统冲突,一般指定特殊数字,例如6666 8888 5678等;
常见的服务使用的端口号:ftp 21;ssh 22;tftp 69;http 80 8080。
是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)
适用情况:适合于对传输质量要求较高,以及传输大量数据的通信。在需要可靠数据传输的场合,通常使用TCP协议微信/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议。
用户数据报协议是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
适用情况:发送小尺寸数据(如对DNS服务器进行IP地址查询时)在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络)适合于广播/组播式通信中。MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输。
共同点:都是传输层的协议;
不同点: TCP是面向连接的,可靠的;
UDP是无连接的,不保证可靠的。
c/s 模型:客户端服务器模型;
b/s 模型:浏览器服务器模型。
服务器流程:
创建流式套接字 socket()
填充网络信息结构体
将服务器的网络信息结构体和套接字绑定 bind()
将套接字设置成被动监听状态 listen()
阻塞等待客户端连接 accept()
收发数据 send/recv
关闭套接字 close()
客户端流程:
创建流式套接字 socket()
填充网络信息结构体
与服务器建立连接 connect()
收发数据 send/recv
关闭套接字 close()
3.1 socket函数
功能:
创建一个套接字
头文件:
#include
#include
函数原型:
int socket(int domain, int type, int protocol);
参数:
domain:
通信域
AF_UNIX 或 AF_LOCAL 本地通信
AF_INET IPV4使用
AF_INET6 IPV6使用
AF_PACKET 原始套接字使用
type:
套接字的类型
SOCK_STREAM TCP使用
SOCK_DGRAM UDP使用
SOCK_RAW 原始套接字使用
protocol:
附加协议 如果没有 传 0 就可以
返回值:
成功 套接字
失败 -1 重置错误码
3.2 bind函数
功能:
将套接字和网络信息结构体绑定
头文件:
#include
#include
函数原型:
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:套接字
addr:网络信息结构体
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
//上面的结构体只是用于强转 防止编译警告
//我们使用的是下面的结构体
struct sockaddr_in {
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 网络字节序的端口号 */
struct in_addr sin_addr; /* 地址 */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* 网络字节序的无符号4字节的IP地址 */
};
addrlen:addr的大小
返回值:
成功 0
失败 -1 重置错误码
3.3 listen函数
功能:
将套接字设置成被动监听状态
头文件:
#include
#include
函数原型:
int listen(int sockfd, int backlog);
参数:
sockfd:套接字
backlog:半连接队列的长度 填多少都可以
一般填 5 10 都行 只要不是 0 就行
返回值:
成功 0
失败 -1 重置错误码
3.4 accept函数
功能:
在半连接队列中获取一个新的连接
如果调用成功,说明有一个新的客户端连到服务器了
这是accept会返回一个新的文件描述符用于和当前的客户端通信
头文件:
#include
函数原型:
int accept(int socket, struct sockaddr * address,
socklen_t * address_len);
参数:
socket:套接字
address:用于保存客户端网络信息结构体的缓冲区的首地址
如果不关心客户端的信息,可以传 NULL
address_len:address的大小 如果不关心时 也要传 NULL
返回值:
成功 用于和新客户端通信的套接字
失败 -1 重置错误码
3.5 send/recv函数
功能:
在套接字上接收一条消息
头文件:
#include
#include
函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
socket:套接字
buf: 用来保存接收的数据的缓冲区的首地址
len: 想要接收的大小
flags: 如果是0的 和read就一样了
如果 MSG_DONTWAIT 非阻塞
返回值:
成功 实际接到的字节数
失败 -1 重置错误码
如果对端关闭了 recv会返回0
下面的三种用法是等价的:
read(sockfd, buff, 128);
recv(sockfd, buff, 128, 0);
recvfrom(sockfd, buff, 128, 0, 0, NULL, NULL);
功能:
向套接字上接收一条数据
头文件:
#include
#include
函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
socket:套接字
buf: 要发送的数据的首地址
len: 想要发送的大小
flags: 如果是0的 和write就一样了
如果 MSG_DONTWAIT 非阻塞
返回值:
成功 实际发送的字节数
失败 -1 重置错误码
如果对端关闭了 第一次send会返回0 第二次send会产生 SIGPIPE 信号
下面的三种用法是等价的:
write(sockfd, buff, 128);
send(sockfd, buff, 128, 0);
sendto(sockfd, buff, 128, 0, 0, NULL, NULL);
3.6 connect
功能:
与服务器建立连接
头文件:
#include
#include
函数原型:
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
socket:套接字
addr: 用于保存服务器网络信息结构体的缓冲区的首地址
addrlen:addr的大小
返回值:
成功 0
失败 -1 重置错误码
4.1 服务器代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(errmsg) \
do \
{ \
printf("%s %s %d\n", __FILE__, __func__, __LINE__); \
perror(errmsg); \
exit(-1); \
} while (0)
int main(int argc, const char *argv[])
{
//入参合理性检查
if (3 != argc)
{
printf("usage : %s \n", argv[0]);
exit(-1);
}
//创建流式套接字
int scokfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == scokfd)
{
ERRLOG("socket error");
}
//填充网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
server_addr.sin_port = htons(atoi(argv[2]));
socklen_t server_addr_len = sizeof(server_addr);
//绑定网络信息结构体
if (-1 == bind(scokfd, (struct sockaddr *)&server_addr, server_addr_len))
{
ERRLOG("bind error");
}
//将套接字设置为被动监听状态
if (-1 == listen(scokfd, 5))
{
ERRLOG("listen error");
}
//创建空的网络信息结构体保存客户端信息
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(client_addr));
socklen_t client_addr_len = sizeof(client_addr);
int ret = 0;
int acceptfd;
char buff[128] = {0};
while (1)
{
//阻塞等待客户端连接
acceptfd = accept(scokfd, (struct sockaddr *)&client_addr, &client_addr_len);
if (-1 == acceptfd)
{
ERRLOG("accept error");
}
printf("有客户端[%s:%d]连入服务器\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
//接收数据
while (1)
{
memset(buff, 0, sizeof(buff));
ret = recv(acceptfd, buff, sizeof(buff), 0);
if (-1 == ret)
{
ERRLOG("recv error");
}
else if (0 == ret)
{
printf("客户端[%s:%d]断开连接\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
break;
}
if (!strcmp(buff, "quit"))
{
printf("客户端[%s:%d]退出连接\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
break;
}
printf("客户端[%s:%d]发来数据:[%s]\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port),buff);
strcat(buff,"--已收到");
if(-1 == send(acceptfd,buff,sizeof(buff),0)){
ERRLOG("send error");
}
}
close(acceptfd);
}
close(scokfd);
return 0;
}
4.2 客户端代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(errmsg) do{\
printf("%s %s %d\n", __FILE__, __func__, __LINE__);\
perror(errmsg);\
exit(-1);\
}while(0)
int main(int argc, const char *argv[]){
//入参合理性检查
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//2.填充服务器的网络信息结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t serveraddr_len = sizeof(serveraddr);
//3.尝试与服务器建立连接
if(-1 == connect(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
ERRLOG("connect error");
}
printf("与服务器建立连接成功..\n");
//进行数据交互
char buff[128] = {0};
while(1){
memset(buff, 0, 128);
fgets(buff, 128, stdin);
buff[strlen(buff) - 1] = '\0';
//发送数据
if(-1 == send(sockfd, buff, 128, 0)){
ERRLOG("send error");
}
if(!strncmp(buff, "quit", 4)){
break;
}
//接收应答
recv(sockfd, buff, 128, 0);
//输出应答消息
printf("应答:[%s]\n", buff);
}
//关闭套接字
close(sockfd);
return 0;
}
TCP底层有一个Nagle算法,将一定短的时间内发往同一个接收端的数据,组装成一个整体发给对方,而接收方没法区分消息的类型,这就可能导致有冲突;
解决方式1,将文件内容 和 结束标志 间隔一段时间再发,但是我们服务器程序里面是坚决不允许使用 sleep 的;
解决方式2,只要保证双发每次收发数据都用一样大的结构 就能解决粘包问题,发送文件结束的标志。
服务器流程:
创建用户数据报套接字
填充服务器的网络信息结构体
将套接字和服务器的网络信息结构体绑定
收发数据 sendto / recvfrom
关闭套接字
客户端流程
创建用户数据报套接字
填充服务器的网络信息结构体
收发数据 sendto recvfrom
关闭套接字
可以理解为是 recv 函数和 accept 函数的结合。
功能:
在套接字上接收一条消息
头文件:
#include
#include
函数原型:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数:
前4个参数和recv的4个参数一模一样
src_addr:用于保存客户端网络信息结构体的缓冲区的首地址
如果不关心客户端的信息,可以传 NULL
addrlen:src_addr的大小 如果不关心 也可以传 NULL
返回值:
成功 实际接到的字节数
失败 -1 重置错误码
recvfrom不会返回 0
可以理解为是 send 函数和 bind 函数的结合。
功能:
向套接字上接收一条数据
头文件:
#include
#include
函数原型:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
前4个参数和send的4个参数一模一样
dest_addr: 消息接受者的网络信息结构体--也就是发送给谁就写谁
addrlen: dest_addr的大小
返回值:
成功 实际发送的字节数
失败 -1 重置错误码
服务器代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(errmsg) do{\
printf("%s %s %d\n", __FILE__, __func__, __LINE__);\
perror(errmsg);\
exit(-1);\
}while(0)
int main(int argc, const char *argv[]){
//入参合理性检查
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//创建用户数据报套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//填充网络信息结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t serveraddr_len = sizeof(serveraddr);
//3.绑定
if(-1 == bind(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
ERRLOG("bind error");
}
//定义结构体保存客户端的信息
//如果使用UDP时不保存客户端的网络信息结构体
//可以接受到客户端发来的数据 但是没法回信给客户端 因为sendto的后两个参数需要用到
struct sockaddr_in clientaddr;
socklen_t clientaddr_len = sizeof(clientaddr);
char buff[128] = {0};
//进行数据交互
while(1){
memset(buff, 0, 128);
//接收数据
if(-1 == recvfrom(sockfd, buff, 128, 0, (struct sockaddr *)&clientaddr, &clientaddr_len)){
ERRLOG("recvfrom error");
}
printf("客户端[%s:%d]发来数据[%s]\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), buff);
//组装应答消息
strcat(buff, "--hqyj");
//回复应答消息
if(-1 == sendto(sockfd, buff, 128, 0, (struct sockaddr *)&clientaddr, clientaddr_len)){
ERRLOG("sendto error");
}
}
//关闭套接字
close(sockfd);
return 0;
}
客户端代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(errmsg) do{\
printf("%s %s %d\n", __FILE__, __func__, __LINE__);\
perror(errmsg);\
exit(-1);\
}while(0)
int main(int argc, const char *argv[]){
//入参合理性检查
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建用户数据报套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//2.填充服务器的网络信息结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t serveraddr_len = sizeof(serveraddr);
//进行数据交互
char buff[128] = {0};
while(1){
memset(buff, 0, 128);
fgets(buff, 128, stdin);
buff[strlen(buff) - 1] = '\0';
//发送数据
if(-1 == sendto(sockfd, buff, 128, 0, (struct sockaddr *)&serveraddr, serveraddr_len)){
ERRLOG("sendto error");
}
//接收应答 客户端无需再保存服务器的网络信息结构体了 因为 serveraddr 没改过
if(-1 == recvfrom(sockfd, buff, 128, 0, NULL, NULL)){
ERRLOG("recvfrom error");
}
//输出应答消息
printf("应答:[%s]\n", buff);
}
//关闭套接字
close(sockfd);
return 0;
}
利用tftpd32拷贝windows文件到虚拟机ubuntu中_Anccccc的博客-CSDN博客将tftpd32放在Windows要做传输文件的目录下,双击运行tftpd32,选择当前win电脑主机IP,保持软件前台运行。编写TFTP客户端,从TFTP服务器上下载文件,即实现利用tftpd32拷贝windows文件到虚拟机ubuntu中。https://blog.csdn.net/Anccccc/article/details/127396872
老师说网络编程在今后的工作中,使用是很频繁的,这两天的学习,主要是熟悉网络传输协议和传输方式,代码量大,尤其是TCP和UDP网络编程,但是结构是很清晰的,主要是考验分析问题,解决问题的能力,需要更缜密的代码逻辑,代码还需要多敲,编程逻辑图还需多画。
写在最后:写这篇文章是笔者一边看老师的目录,一边回想老师讲的内容,仅仅选取了我自己认为比较重要的,或者自己之前没接触过的进行汇总、总结,知识体系不完善,内容也不详细,仅供笔者复习使用。如果是有需要笔记,或者对这方面感兴趣,可以私信我,发你完整的知识体系和详细内容的笔记。写的仓促、水平有限,如有任何错误请多指正,也欢迎友好交流,定会虚心听取大佬的宝贵意见!