UDP协议
UDP
协议处于传输层,是不可靠谱、无连接、消息有边界的协议
TCP
类似于管道,UDP
类似于队列
UDP头部
传输层头部都不需要IP地址,都只需要端口号
Berkeley Socket(库)
Berkeley Scoket 库已经完成了传输层之下的内容,我们只需要再应用层调用下面提供的服务接口即可
Socket :套接字,建立连接时使用
地址:链路层使用MAC地址、网络层使用IP地址、传输层使用端口号
应用层的地址:IP地址加上端口号
IPv4的地址格式
sin_family
:地址的类型AF_INET代表IPv4
sin_port
:端口号(使用网络字节序)16Byte(2字节)
sin_addr
:IP地址(使用网络字节序)32Byte(4字节)
网络字节序
大端法存储数据被称为网络字节序
大小端都可以存储数据的为主机字节序
数据如果要从主机到网络,那么我们就要将小端数据变成大端数据
h host主机
n net网络
s short 2B 16bit
l int 4B 32bit
大小端转换
我们要用printf()
就使用小端;如果我们使用sockaddr
就使用大端
const char *cp
:点分十进制
struct in_addr *inp
:存储结果的地址
inet_aton(const char * cp ,struct in_addr * np)
:点分十进制—>32位大端整数,结果作为参数
inet_addr
:点分十进制—>32位大端整数,结果作为返回值
char *inet_ntoa
:把32位大端整数转换为字符串
IP地址实现大小端转换
IP地址以及端口实现大小端转换
可以使用sudo cat /etc/hosts
查看本地存在的域名映射
从代码中获取域名对应的IP地址
底层使用了DNS服务器,断网的时候会报错,这个函数出错了不能用perror,报错返回空指针
可以根据这个函数获取主机信息
使用gethostbyname获取远程目标主机的信息
可以将IPv4或者IPv6转换位字符串
基于TCP的网络通信
网络的用法和文件是一样的
socket(系统调用)
创建一个用于网络传输的文件对象
domain
:指的是你所使用的领域,一般使用AF_INET表示IPv4
type
:
SOCK_STREAM
:TCP协议
SOCK_DGRAM
:UDP协议
protocol
:填0表示自动获取
connect
建立连接,发送第一次握手
sockfd
:填的值是socket的返回值
addr 和addrlen
描述目标地址
Linux上使用抓包操作查看自己发送的数据包
tcpdump -i 网卡名称 port 端口号
tcp的标志
根据四元组<源IP,源端口号,目的IP,目的端口>
来区分两条不同的TCP连接,只要四元组中有一个不一样那就是不同的TCP连接
服务端的IP和端口一般是固定的—>这不影响并发连接数量的上限
bind(服务端)
让服务端端绑定一个IP:port给socket ,以便于客户端每次都可以固定的和同一个端口连接服务端
绑定端口之后客户端还不能连接服务端,还需要让服务端调用listen系统调用
listen(服务端调用)
listen
是说明让socket
当作服务端来使用
服务端调用listen
之后客户端才能connect
listen
会把socket
文件对象清空,会重新组织成两个数据结构,一个是半连接队列,一个是全连接队列
当服务端调用listen
,接收到客户端的connect
请求,服务端会像客户端发回ACK
应答,此时这个客户端就会进入如半连接队列里面,当客户端完成了三次握手的最后一个ACK
是服务端就会把这个连接放入连接队列里面
backlog
:半连接队列的长度
DDOS攻击
SYN flood
泛洪
客户端你只对服务器只发送第一次握手,但是不发生送第三次握手,方客户端越来越多的时候服务器端的半连接队列就会被充满,以至于服务器无法在继续连接真难正需要连接的客户端
如何防御DDOS攻击?
(1)增发半连接队列(不可取)
(2)使用SYN cookies
accept
上面我们已经了解到我们可以使用socket
为客户端创建一个用于网络连接的文件,对于服务端首先会使用bind
为客户端绑定固定端口,然后再利用listen
改造客户端的socket
文件对象,建立连接之后便会把客户端建立的连接放入全连接队列。
接下来调用accept
从全连接队列取出一个连接,根据这个连接创建一个已链接socket
因此就会有两个文件描述符一个是socketFd
(用于建立连接),一个是netFd
(用于发送数据)
accept
的本质是一个read
,如果全连接队列为空,accept
就会阻塞,当全连接队列不为空就会停止阻塞
sockfd
:listen 的 fd
addr
:addr填本地地址,如果填了本地地址就可以获取对端地址,如果不想获取对端地址就填NULL
addrlen
:先申请一个socklen_t的变量,再取地址传递,因为他是一个传入传出参数,并且socklen_t的长度不能嫩能够填0,要填addr的长度
读写网络缓冲区
write/read
可以用于客户端的socket以及服务端端的已连接socket,不可以用于listensocket
send和recv
只能操作socket
write和read可以操作文件和socket以及管道等
send和write
flages
:控制send的行为,如果为0,不做任何行为
在对socket操作时 send(… , 0)和write是等价的
send/write
不是往网络中发数据,其本质是拷贝数据到SND
(发送)缓冲区,发不发送由内核协议栈决定是否发向网络
read和recv
read/recv
是拷贝数据从RCV缓冲区到用户空间,网络中如果没有数据就会发生阻塞
使用socket实现网络连接
如何使接收方和发送方知道相同的信息,就需要在应用层制定协议
利用socket实现即时聊天
基本 上和管道时一样的,socket的R,W的fd是同一个
使用IO多路复用来防止stdin阻塞
TIME_WAIT
主动关闭方在四次挥手之后会进入TIME_WAIT
状态
服务端如果主动关闭,会进入TIME_WAIT
(固定60S),服务端在time_wait
状态,不允许bind
同样的IP
和端口
,避免在这个时间之内他们两方在建立新的连接,但是旧连接的数据可能就会在新连接里传输
但是每次客户端发起新的连接都是从不同的端口进行发出,因此每次发起连接的四元组都是不一样的,因此我们没有必要让服务端进入TIME_WAIT
让服务端你无视TIME_WAIT
修改socket的属性(在bind之前修改)
使用setsockopt修改socket的属性
sockfd
:listen socket,监听描述符level
:直接填SOL_SOCKET,表示修改的是socket这一层(传输层)optname
:要修改哪个属性
optval
:要改成什么样optlen
:属性的长度
服务端不是屏蔽TIME_WAIT而只是忽略TIME_WAIT
实现断线重连
服务端使用select
监听sockfd
—>读事件,如果都就绪,说明调用accept
不会阻塞
#include <43func.h>
//服务端
int main(int argc,char * argv[]) {
ARGS_CHECK(argc,3);
int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建你IPv4的TCP连接
ERROR_CHECK(sockfd,-1,"socket");
int optval = 1;
int ret = setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(int));
ERROR_CHECK(ret,-1,"setsockopt");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
ret = bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
ERROR_CHECK(ret,-1,"bind");
ret = listen(sockfd,10);//更改server端socket的结构,更改半连接队列长度为10
ERROR_CHECK(ret,-1,"listen");
//accept要放在select后面
//去使用时确保从标准输入中输入数据在客户端建立连接之后
//accept之后创建新的netfd,这个netfd加入监听--->分离监听和就绪
//客户端如果断开连接以后,服务端不要退出,要取消监听netfd
fd_set rdset;//保存就绪的fd
fd_set monitorSet;//使用一个单独的监听集合
FD_ZERO(&monitorSet);//初始化rdset
FD_SET(STDIN_FILENO,&monitorSet);//将文件输入流,添加到监听队列.防止阻塞
char buf[4096] = {0};
int netfd = -1;
while(1) {
memcpy(&rdset,&monitorSet,sizeof(fd_set));//初始化数组
select(20,&rdset,NULL,NULL,NULL);//开启监听
if(FD_ISSET(STDIN_FILENO,&rdset)){
bzero(buf,sizeof(buf));//清空数组
ret = read(STDIN_FILENO,buf,sizeof(buf));//读取输入流数据
if(ret == 0) {
send(netfd,"nishigehaoren",13,0);
break;
}
send(netfd,buf,strlen(buf),0);//向网络文件中写入数据
}
if(FD_ISSET(sockfd,&rdset)){
netfd = accept(sockfd,NULL,NULL);//创建socket文件对象
ERROR_CHECK(netfd,-1,"accept");
FD_SET(netfd,&monitorSet);//将netfd加入监听集合
puts("new connect is accepted!\n");
}
if(FD_ISSET(netfd,&rdset)) {
bzero(buf,sizeof(buf));//清空buf
ret = read(netfd,buf,sizeof(buf));//读取网络文件中的数据
if(ret == 0) {
puts("bye bye");
FD_CLR(netfd,&monitorSet);//将netfd取消监听
close(netfd);
netfd = -1;
continue;
}
puts(buf);
}
}
close(sockfd);
close(netfd);
}
实现群聊
UDP实现聊天
bind
:固定服务端的IP:port
sendto
:客户端先sendto
UPD中socket的用法
client
server