3.1. 使用 TCP 协议的流程
服务端:socket → bind → listen → while(1){ → accept → recv → send → close → } → close
客户端:socket→ → → → → → → → → → → →connect → send → recv → → → → → close
1. TCP 通信的基本步骤 | |||
---|---|---|---|
服务器: | |||
2. socket 函数:生成一个套接口描述符 | 3. bind 函数:用来绑定一个端口号和 IP 地址 | 4. listen 函数: 使服务器的这个端口和 IP 处于监听状态 | 实例1 : 使本地ip处于LISTEN(监听)状态 |
5. accept 函数:接受远程连接请求, 建立起通信连接 | 6. recv 函数: 接收储存传来的数据 | 7. send 函数:用新的套接字发送数据给指定的远端主机 | 8. close 函数:关闭文件 |
客户端: | |||
1. connect 函数:请求连接远程服务器 | 实例2 : 实现tcp服务器端,客户端各发一条数据给对方 | 实例3 : 使用tcp实现即时聊天 | 实例4:使用tcp实现即时聊天,客户端支持断开重连(使用select) |
3.2. 使用 UDP 协议的流程
服务端:socket → bind → recvfrom →→→→→ sendto → close
客户端:socket →→→→→→→→→ sendto → recvfrom → close
1. sendto()函数向服务器发送数据 | 2. recvfrom()函数接收服务器的数据 | 实例5:udp实现客户端和服务器端传话 | 实例6:udp实现客户端和服务器端传话 |
---|
3.3. 设置套接口的选项 setsockopt 的用法
函数 setsockopt 设置选项值 实例7:使用SO_REUSEADDR实现重用本地地址和端口
3.4. 单播、广播、组播(多播)(不常用)
3.5. 什么是 DDos(SYN Flooding)攻击,如何防护
下面仅作了解
3.6. 描述符属性修改及文件描述符的传递
1. fcntl 函数 | 2. Socketpair 函数简介 | 3. Sendmsg 函数简介 | 4. Recvmsg |
---|---|---|---|
5. Writev | 6.Readv | 7. Cmsg 用来设定我们的*msg_control 指针 |
3.1. 使用 TCP 协议的流程
**1. TCP 通信的基本步骤如下: 服务端:socket → bind → listen → while(1){ → accept → recv → send → close → } → close 客户端:socket→ → → → → → → → → → → →connect → send → recv → → → → → close**netstat -an | grep ^tcp -查看TCP协议的监听
客户端一对一 服务器, 服务器一对多客户端
服务器端:
头文件:
#include
#include
#include
#include
2. socket
函数:生成一个套接口描述符
int socket(int domain,int type,int protocol);
作用:生成一个套接口描述符。
参数:
domain
:AF_INET
:Ipv4 网络协议AF_INET6
:IPv6 网络协议type
:SOCK_STREAM
( tcp )SOCK_DGRAM
( udp )protocol
:**返回值:**成功则返回套接口描述符,失败返回-1。
常用实例:
int sfd = socket(AF_INET, SOCK_STREAM, 0); -打开之前, sfd = 3,打开一个 sfd 为 4, 即第一个没被占用的描述符
if(sfd == -1)
{
perror("socket");
exit(-1);
}
3. bind 函数:用来绑定一个端口号和 IP 地址
int bind(int sockfd,struct sockaddr * my_addr,socklen_t addrlen);
作用:用来绑定一个端口号和 IP 地址,使套接口与指定的端口号和 IP 地址相关联。
(只能绑定本地的IP地址, 不能绑定其他机器)
参数:
sockfd
: 为前面 socket 的返回值。my_addr
: 为结构体指针变量对于不同的 socket domain 定义了一个通用的数据结构
struct sockaddr -此结构体不常用
{
unsigned short int sa_family; -调用 socket()时的 domain 参数,即 AF_INET 值。
char sa_data[14]; -最多使用 14 个字符长度 , 这个地方自己拼接信息, 不友好
};
此 sockaddr
结构会因使用不同的 socket domain 而有不同结构定义,
例如使用 AF_INET
domain ( Ipv4 ),其 socketaddr
结构定义便为
struct sockaddr_in -常用的结构体
{
unsigned short int sin_family; -即为 sa_family ➔AF_INET
uint16_t sin_port; -为使用的 port 编号
struct in_addr sin_addr; -为 IP 地址
unsigned char sin_zero[8]; -未使用
};
struct in_addr
{
uint32_t s_addr;
};
addrlen
: sockaddr
的结构体长度。通常是计算 sizeof(struct sockaddr);
返回值:成功则返回 0,失败返回-1
常用实例:
struct sockaddr_in my_addr; -定义结构体变量
memset(&my_addr, 0, sizeof(struct sockaddr)); -将结构体清空
---或 bzero(&my_addr, sizeof(struct sockaddr));
my_addr.sin_family = AF_INET; -表示采用 Ipv4 网络协议
my_addr.sin_port = htons(8888); -表示端口号为 8888,通常是大于 1024 的一个值。
---htons()用来将参数指定的 16 位 hostshort 转换成网络字符顺序
my_addr.sin_addr.s_addr = inet_addr("192.168.0.101");
---inet_addr()用来将IP地址字符串转换成网络所使用的二进制数字,如果为 INADDR_ANY,这表示服务器自动填充本机 IP 地址。
if(bind(sfd, (struct sockaddr*)&my_str, sizeof(struct socketaddr)) == -1) {
perror("bind");
close(sfd);
exit(-1);
}
(注:通过将 my_addr.sin_port
置为 0,函数会自动为你选择一个未占用的端口来使用。同样,通过将 my_addr.sin_addr.s_addr
置为 INADDR_ANY
,系统会自动填入本机 IP 地址。)
4. listen 函数: 使服务器的这个端口和 IP 处于监听状态
int listen(int sockfd,int backlog);
作用 : 使服务器的这个端口和 IP 处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接。
参数:
sockfd
: 为前面 socket
的返回值.即 sfd
返回值: 成功则返回 0,失败返回-1
常用实例:
if(listen(sfd, 10) == -1)
{
perror("listen");
close(sfd);
exit(-1);
}
本地给本地发包测试 , 为了避免源地址和目的地址一样, 目的地址将被改为环回地址 , 环回地址是主机用于向自身发送通信的一个特殊地址。
实例1 : 使本地ip处于LISTEN(监听)状态
int main(int argc, char *argv[])
{
ARGS_CHECK(argc, 3);
int socketFd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(socketFd, -1, "socket"); -类似open
struct sockaddr_in serAddr;
bzero(&serAddr, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(atoi(argv[2])); -htonl不可以,接到的数不对导致出错
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
int ret;
ret = bind(socketFd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "bind");
listen(socketFd, 10);
while(1); -为了进行测试,让程序卡住
return 0;
}
5. accept 函数:接受远程连接请求, 建立起通信连接
作用 :接受远程计算机的连接请求,建立起与客户机之间的通信连接。
( 服务器处于监听状态时, 如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。)
(当 accept
函数接受一个连接时,会返回一个新的 socket 标识符, 以后的数据传输和读取就要通过这个新的 socket 编号来处理,原来参数中的 socket 也可以继续使用,继续监听其它客户机的连接请求。)
(也就是说,类似于移动营业厅,如果有客户打电话给 10086,此时服务器就会请求连接,处理一些事务之后,就通知一个话务员接听客户的电话,也就是说,后面的所有操作,此时已经于服务器没有关系,而是话务员跟客户的交流。)
(对应过来,客户请求连接我们的服务器, 我们服务器先做了一些绑定和监听等等操作之后,如果允许连接,则调用 accept 函数产生一个新的套接字,然后用这个新的套接字跟我们的客户进行收发数据。)
(也就是说,服务器跟一个客户端连接成功,会有两个套接字。)
int accept(int s,struct sockaddr * addr,socklen_t* addrlen);
参数:
s
: 为前面 socket
的返回值 , 即 sfd
addr
: 为结构体指针变量,和 bind 的结构体是同种类型的,系统会把远程主机的信息(远程 主机的地址和端口号信息)保存到这个指针所指的结构体中。addrlen
: 表示结构体的长度,为整型指针, 数据大小返回值:成功则返回新的 socket 处理代码 new_fd
,失败返回-1
常用实例:
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
if(new_fd == -1)
{
perror("accept");
close(sfd);
exit(-1);
}
printf("%s %d success connect\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
6. recv 函数: 接收储存传来的数据
int recv(int sockfd,void *buf,int len,unsigned int flags);
作用:用新的套接字来接收远端主机传来的数据,并把数据存到由参数 buf 指向的内存空间
参数:
sockfd
: 为前面 accept
的返回值, 即 new_fd
,也就是新的套接字。buf
; 表示缓冲区len
: 表示缓冲区的长度flags
: 调用操作方式,通常为 0返回值:成功则返回实际接收到的字符数,可能会少于你所指定的接收长度。失败返回-1
常用实例:
char buf[512] = {0};
if(recv(new_fd, buf, sizeof(buf), 0) == -1)
{
perror("recv");
close(new_fd);
close(sfd);
exit(-1);
}
puts(buf);
}
7. send 函数:用新的套接字发送数据给指定的远端主机
int send(int s,const void * msg,int len,unsigned int flags);
作用 : 用新的套接字发送数据给指定的远端主机
参数:
s
: 为前面 accept 的返回值.即 new_fdmsg
: 一般为常量字符串len
: 表示长度flags
: 调用操作方式,通常为 0返回值:成功则返回实际传送出去的字符数,可能会少于你所指定的发送长度。失败返回 -1
常用实例:
if(send(new_fd, "hello", 6, 0) == -1)
{
perror("send");
close(new_fd);
close(sfd);
exit(-1);
}
8. close 函数:关闭文件
int close(int fd);
作用 : 当使用完文件后若已不再需要则可使用 close()关闭该文件,并且 close()会让数据写回磁盘, 并释放该文件所占用的资源
参数:
fd
: 为前面的 sfd
或者 new_fd
返回值: 若文件顺利关闭则返回 0,发生错误时返回-1
常用实例:
close(new_fd);
close(sfd);
可以调用 shutdown 实现半关闭
int shutdown(int sockfd, int how);
函数行为由how值决定。
SHUT_RD
:值为0,关闭连接的读这一半,套接字中不再有数据接收,且套接字接收缓冲区中的现有数据全都被丢弃,该套接字描述符不能再被进程调用,对端发送的数据会被确认,然后丢弃。
SHUT_WR
:值为1,关闭连接的写这一半。这称为半关闭,当前在套接字发送缓冲区数据被发送,然后连接终止序列。不论套接字描述符引用技术是否等于0,写半部都会被关闭。
SHUT_RDWR
:值为2,连接的读和写都关闭。相当于先调用SHUT_RD
,再调用SHUT_WR
。
客户端:
1. connect 函数:请求连接远程服务器
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
作用 : 用来请求连接远程服务器,将参数 sockfd 的 socket 连至参数 serv_addr 指定的服务器 IP 和端口号上去。
参数:
sockfd
: 为前面 socket 的返回值,即 sfdserv_addr
: 为结构体指针变量,存储着远程服务器的 IP 与端口号信息。addrlen
: 表示结构体变量的长度返回值:成功则返回 0,失败返回-1
常用实例:
struct sockaddr_in seraddr; -请求连接服务器
memset(&seraddr, 0, sizeof(struct sockaddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8888); -服务器的端口号
seraddr.sin_addr.s_addr = inet_addr("192.168.0.101"); -服务器的 ip
if(connect(sfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr)) == -1)
{
perror("connect");
close(sfd);
exit(-1);
}
客户端一对一 , 服务器一对多, accept
多个客户端, 每个 newFd
对应一个客户端
实例2 : 实现tcp服务器端,客户端各发一条数据给对方
服务器:
int main(int argc, char *argv[])
{
ARGS_CHECK(argc, 3);
int socketFd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(socketFd, -1, "socket"); -类似open
struct sockaddr_in serAddr;
bzero(&serAddr, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(atoi(argv[2])); -htonl不可以,接到的数不对导致出错
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
int ret;
ret = bind(socketFd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "bind");
listen(socketFd, 10);
struct sockaddr_in client; -拿到对端的信息
bzero(&client, sizeof(client));
socklen_t clientLen = sizeof(client);
int newFd = accept(socketFd, (struct sockaddr*)&client, &clientLen);
printf("client ip = %s, client port = %d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
char buf[1024] = {0};
recv(newFd, buf, sizeof(buf),0);
printf("server gets %s\n", buf);
send(newFd, "I am Jocker", 11, 0);
//while(1); -用来测试查看tcp状态
close(newFd);
close(socketFd);
return 0;
}
客户端:
int main(int argc, char *argv[])
{
ARGS_CHECK(argc, 3);
int socketFd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(socketFd, -1, "socket"); -类似open
struct sockaddr_in serAddr;
bzero(&serAddr, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(atoi(argv[2])); -htonl不可以,接到的数不对导致出错
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
int ret;
ret = connect(socketFd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "connect");
send(socketFd, "I am Queen", 10, 0);
char buf[1024] = {0};
recv(socketFd, buf, sizeof(buf), 0);
printf("client gets %s\n", buf);
// while(1); -用作测试
close(socketFd);
return 0;
}
bind
中强制类型转换是因为serAddr
是 struct sockaddr_in
类型 , 而 bind
接口参数类型是 struct sockaddr *
recv
send
用法类似read
write
用法
使用两个窗口分别启动 服务器 和 客户端
服务器输入: ./server + 本地IP地址
客户端输入: ./client + 本地IP地址
运行结果 :
服务器:
client ip = 192.168.220.128, client port = 35930
server gets I am Queen
客户端:
client gets I am Jocker
实例3 : 使用tcp实现即时聊天
首先封装一个接口
int tcpInit(int *sfd, char *ip, char *port)
{
int socketFd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(socketFd, -1, "socket");
struct sockaddr_in serAddr;
bzero(&serAddr, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(atoi(port));
serAddr.sin_addr.s_addr = inet_addr(ip);
int ret;
ret = bind(socketFd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "bind");
listen(socketFd, 10);
*sfd = socketFd;
return 0;
}
服务器:
int tcpInit(int*, char*, char*);
int main(int argc, char* argv[])
{
ARGS_CHECK(argc, 3);
int socketFd;
int ret;
ret = tcpInit(&socketFd,argv[1],argv[2]);
if(-1 == ret)
{
return -1;
}
int newFd = accept(socketFd, NULL, NULL);
fd_set rdset;
char buf[10];
while(1)
{
if(strlen(buf) == 0)
{
printf("connect success\n");
}
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO, &rdset);
FD_SET(newFd, &rdset);
ret = select(newFd + 1, &rdset, NULL, NULL, NULL);
if(ret > 0)
{
if(FD_ISSET(STDIN_FILENO, &rdset))
{
bzero(buf, sizeof(buf));
ret = read(STDIN_FILENO, buf, sizeof(buf));
if(0 == ret)
{
printf("You exit. chat end...\n");
break;
}
send(newFd, buf, strlen(buf) - 1, 0);
}
if(FD_ISSET(newFd, &rdset))
{
bzero(buf,sizeof(buf));
ret = recv(newFd, buf, sizeof(buf), 0);
if(0 == ret) -判断对方断开
{
printf("Peer exit. chat end...\n");
break;
}
printf("%s\n", buf);
}
}
}
return 0;
}
客户端:
int main(int argc, char *argv[])
{
ARGS_CHECK(argc, 3);
int socketFd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(socketFd, -1, "socket");
struct sockaddr_in serAddr;
bzero(&serAddr, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(atoi(argv[2]));
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
int ret;
ret = connect(socketFd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "connect");
fd_set rdset;
char buf[1024];
int time = 0;
while(1)
{
if(time == 0)
{
printf("connect success\n");
}
++time;
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO, &rdset);
FD_SET(socketFd, &rdset);
ret = select(socketFd + 1, &rdset, NULL, NULL, NULL);
if(ret > 0)
{
if(FD_ISSET(STDIN_FILENO, &rdset))
{
bzero(buf, sizeof(buf));
ret = read(STDIN_FILENO, buf, sizeof(buf));
if(0 == ret)
{
printf("You exit. chat end...\n");
break;
}
send(socketFd, buf, strlen(buf) - 1, 0);
}
if(FD_ISSET(socketFd, &rdset))
{
bzero(buf,sizeof(buf));
ret = recv(socketFd, buf, sizeof(buf), 0);
if(0 == ret)
{
printf("Peer exit. chat end...\n");
break;
}
printf("%s\n", buf);
}
}
}
close(socketFd);
return 0;
}
使用两个窗口分别启动 服务器 和 客户端
服务器输入: ./server + 本地IP地址
客户端输入: ./client + 本地IP地址
运行结果 :
服务器:
connect success
hi
can you hear me
yes
I went offline first
bye
客户端:
connect success
hi
can you hear me
yes
I went offline first
bye
Peer exit. chat end…
实例4:使用tcp实现即时聊天,客户端支持断开重连(使用select)
还是使用之前的封装函数 int tcpInit(int *sfd, char *ip, char *port)
服务器:
int tcpInit(int*, char*, char*);
int main(int argc, char* argv[])
{
ARGS_CHECK(argc, 3);
int socketFd;
int ret;
ret = tcpInit(&socketFd,argv[1],argv[2]);
if(-1 == ret)
{
return -1;
}
int newFd;
fd_set rdset;
fd_set needMoniterFd; -始终记录要监控的描述符
FD_ZERO(&needMoniterFd);
FD_SET(STDIN_FILENO, &needMoniterFd);
FD_SET(socketFd, &needMoniterFd);
char buf[1024];
while(1)
{
memcpy(&rdset, &needMoniterFd, sizeof(fd_set));
ret = select(14, &rdset, NULL, NULL, NULL);
if(ret > 0)
{
if(FD_ISSET(socketFd, &rdset))
{
newFd = accept(socketFd, NULL, NULL);
ERROR_CHECK(newFd, -1, "accept");
FD_SET(newFd, &needMoniterFd);
printf("connect success\n");
}
if(FD_ISSET(STDIN_FILENO, &rdset))
{
bzero(buf, sizeof(buf));
ret = read(STDIN_FILENO, buf, sizeof(buf));
if(0 == ret)
{
printf("You exit. chat end...\n");
break;
}
send(newFd, buf, strlen(buf) - 1, 0);
}
if(FD_ISSET(newFd, &rdset))
{
bzero(buf,sizeof(buf));
ret = recv(newFd, buf, sizeof(buf), 0);
if(0 == ret) -判断对方断开
{
printf("Peer exit....\n");
FD_CLR(newFd, &needMoniterFd);
close(newFd);
}else{
printf("%s\n", buf);
}
printf("%s\n", buf);
}
}
}
return 0;
}
客户端:
int main(int argc, char *argv[])
{
ARGS_CHECK(argc, 3);
int socketFd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(socketFd, -1, "socket"); //类似open
struct sockaddr_in serAddr;
bzero(&serAddr, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(atoi(argv[2])); //htonl不可以,接到的数不对导致出错
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
int ret;
ret = connect(socketFd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "connect");
fd_set rdset;
char buf[1024];
int time = 0;
while(1)
{
if(time == 0)
{
printf("connect success\n");
}
++time;
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO, &rdset);
FD_SET(socketFd, &rdset);
ret = select(socketFd + 1, &rdset, NULL, NULL, NULL);
if(ret > 0)
{
if(FD_ISSET(STDIN_FILENO, &rdset))
{
bzero(buf, sizeof(buf));
ret = read(STDIN_FILENO, buf, sizeof(buf));
if(0 == ret)
{
printf("You exit. chat end...\n");
break;
}
send(socketFd, buf, strlen(buf) - 1, 0);
}
if(FD_ISSET(socketFd, &rdset))
{
bzero(buf,sizeof(buf));
ret = recv(socketFd, buf, sizeof(buf), 0);
if(0 == ret)
{
printf("Peer exit. chat end...\n");
break;
}
printf("%s\n", buf);
}
}
}
close(socketFd);
return 0;
}
使用两个窗口分别启动 服务器 和 客户端
服务器输入: ./server + 本地IP地址
客户端输入: ./client + 本地IP地址
运行结果 :
服务器:
connect success
Are ya ready kids?
Aye Aye Captain.
I can’t hear you.
Aye Aye Captain!
OHHHHHHH, Who lives in a pineapple under the sea?
SpongeBob SquarePants!
Peer exit…connect success
Absorbent and yellow and porous is he!
SpongeBob SquarePants!
客户端:
connect success
Are ya ready kids?
Aye Aye Captain.
I can’t hear you.
Aye Aye Captain!
OHHHHHHH, Who lives in a pineapple under the sea?
SpongeBob SquarePants
^C
中途^C 退出客户端,然后重新输入: ./client + 本地IP地址
运行
connect success
Absorbent and yellow and porous is he!
SpongeBob SquarePants!
Peer exit. chat end…
就完成了客户端断开重连
3.2. 使用 UDP 协议的流程图
UDP 通信流程图如下:
服务端:socket → bind → recvfrom →→→→→ sendto → close
客户端:socket →→→→→→→→→ sendto → recvfrom → close
服务器一对多客户端,客户端描述符一对多服务器
1. sendto()
函数
int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, socklen_t tolen);
作用:向服务器发送数据
参数:
sockfd
: socket
函数的返回值msg
: 一般为常量字符串len
: 表示长度flags
: 调用操作方式,通常为 0send()
函数多了两个参数)to
:指向接收数据的主机地址信息的结构体(sockaddr_in需类型转换),一般表示目地机的 IP 地址和端口号信息tolen
:to所指结构体的长度,常被赋值为 sizeof (struct sockaddr)
返回值:sendto
函数也返回实际发送的数据字节长度,在出现发送错误时返回-1。
2. recvfrom()
函数
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);
作用:接收服务器的数据
参数:
sockfd
: 为前面 accept
的返回值, 即 new_fd
,也就是新的套接字。buf
; 表示缓冲区len
: 表示缓冲区的长度flags
: 调用操作方式,通常为 0from
:指向发送数据的客户端地址信息的结构体(sockaddr_in需类型转换),是一个 struct sockaddr
类型的变量,该变量保存连接机的IP地址及端口号。fromlen
:指针,指向from结构体长度值,常置为 sizeof (struct sockaddr)。返回值:
recvfrom()
返回时,fromlen
包含实际存入 from
中的数据字节数。recvfrom()
函数返回接收到的字节数或当出现错误时返回-1,并置相应的 errno。实例5:udp实现客户端和服务器端传话
服务器:
int main(int argc, char* argv[])
{
ARGS_CHECK(argc, 3);
int socketFd=socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(socketFd, -1, "socket");
struct sockaddr_in serAddr;
bzero(&serAddr, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(atoi(argv[2]));
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
int ret;
ret = bind(socketFd, (struct sockaddr*)&serAddr, sizeof(serAddr)); -端口激活
ERROR_CHECK(ret, -1, "bind");
struct sockaddr_in client;
bzero(&client, sizeof(client));
socklen_t fromLen = sizeof(struct sockaddr);
char buf[128] = {0};
recvfrom(socketFd, buf, sizeof(buf), 0, (struct sockaddr*)&client, &fromLen);
printf("gets [%s]\nclient ip = %s, port = %d\n", buf,inet_ntoa(client.sin_addr), ntohs(client.sin_port));
sendto(socketFd, "I am Joker", 10, 0, (struct sockaddr*)&client, sizeof(client));
close(socketFd);
return 0;
}
客户端:
int main(int argc, char* argv[])
{
ARGS_CHECK(argc, 3);
int socketFd=socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(socketFd, -1, "socket");
struct sockaddr_in serAddr;
bzero(&serAddr, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(atoi(argv[2]));
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
char buf[128] = {0};
sendto(socketFd, "I am client", 11, 0, (struct sockaddr*)&serAddr, sizeof(struct sockaddr));
recvfrom(socketFd, buf, sizeof(buf), 0, NULL, NULL);
printf("client gets [%s]\n", buf);
close(socketFd);
return 0;
}
使用两个窗口分别启动 服务器 和 客户端
服务器输入: ./server + 本地IP地址
客户端输入: ./client + 本地IP地址
运行结果 :
服务器:
gets [I am client]
client ip = 192.168.1.1, port = 49999
客户端:
client gets I am Jocker
实例6:udp实现客户端和服务器端传话
服务器:
int main(int argc, char* argv[])
{
ARGS_CHECK(argc, 3);
int socketFd = socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(socketFd, -1, "socket");
struct sockaddr_in serAddr;
bzero(&serAddr, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(atoi(argv[2]));
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
int ret;
ret = bind(socketFd, (struct sockaddr*)&serAddr, sizeof(serAddr)); -端口激活
ERROR_CHECK(ret, -1, "bind");
struct sockaddr_in client;
bzero(&client, sizeof(client));
socklen_t fromLen = sizeof(struct sockaddr);
char buf[128] = {0};
recvfrom(socketFd, buf, sizeof(buf), 0, (struct sockaddr*)&client, &fromLen);
printf("gets [%s]\nclient ip = %s, port = %d\n", buf,inet_ntoa(client.sin_addr), ntohs(client.sin_port));
fd_set rdset;
while(1)
{
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO, &rdset);
FD_SET(socketFd, &rdset);
ret = select(socketFd + 1, &rdset, NULL, NULL, NULL);
if(FD_ISSET(STDIN_FILENO, &rdset))
{
bzero(buf, sizeof(buf));
read(STDIN_FILENO, buf, sizeof(buf) - 1);
sendto(socketFd, buf, strlen(buf) - 1, 0, (struct sockaddr*)&client, sizeof(client));
}
if(FD_ISSET(socketFd, &rdset))
{
bzero(buf, sizeof(buf));
recvfrom(socketFd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&client, &fromLen);
printf("%s\n", buf);
}
}
close(socketFd);
return 0;
}
客户端:
int main(int argc, char* argv[])
{
ARGS_CHECK(argc, 3);
int socketFd = socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(socketFd, -1, "socket");
struct sockaddr_in serAddr;
bzero(&serAddr, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(atoi(argv[2]));
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
char buf[128] = {0};
sendto(socketFd, "1", 1, 0, (struct sockaddr*)&serAddr, sizeof(struct sockaddr));
fd_set rdset;
while(1)
{
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO, &rdset);
FD_SET(socketFd, &rdset);
select(socketFd + 1, &rdset, NULL, NULL, NULL);
if(FD_ISSET(STDIN_FILENO, &rdset))
{
bzero(buf, sizeof(buf));
read(STDIN_FILENO, buf, sizeof(buf));
sendto(socketFd, buf, strlen(buf)-1, 0, (struct sockaddr*)&serAddr, sizeof(serAddr));
}
if(FD_ISSET(socketFd, &rdset))
{
bzero(buf, sizeof(buf));
recvfrom(socketFd, buf, sizeof(buf), 0, NULL, NULL );
printf("%s\n", buf);
}
}
close(socketFd);
return 0;
}
使用两个窗口分别启动 服务器 和 客户端
服务器输入: ./server + 本地IP地址
客户端输入: ./client + 本地IP地址
运行结果 :
服务器:
128 2000
gets [1]
client ip = 192.168.1.1, port = 36666
can you hear me
yes
bye
客户端:
can you hear me
yes
bye
其中某一断断开对另一端没有影响
注意:
操作系统的 UDP 接收流程如下:
收到一个 UDP 包后,验证没有错误后,放入一个包队列中,队列中的每一个元素就是一个完整的 UDP 包。当应用程序通过 recvfrom()
读取时, OS 把相应的一个完整 UDP 包取出,然后拷贝到用户提供的内存中,物理用户提供的内存大小是多少,OS 都会完整取出一个 UDP 包。如果用户提供的内存小于这个 UDP 包的大小,那么在填充慢内存后,UDP 包剩余的部分就会被丢弃,以后再也无法取回。
(TCP像管道,UDP像消息队列)
这与 TCP 接收完全不同,TCP 没有完整包的概念,也没有边界,OS 只会取出用户要求的大小,剩余的仍然保留在 OS 中,下次还可以继续取出。
思考题: 机器 A 向机器 B 发送数据,以 TCP 方式发送 3 个包,对方可能会收到几个包,以 UDP 方式发送 3 个包,对方可能会收到几个包
(TCP至少3个, 3,6,9,…个包,UDP 0-3 个包)
3.3. 设置套接口的选项 setsockopt 的用法
头文件:
#include
#include
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
作用:用于任意类型、任意状态套接口的设置选项值
参数:
sockfd
:标识一个套接口的描述符level
:选项定义的层次;支持 SOL_SOCKET
、IPPROTO_TCP
、IPPROTO_IP
和 IPPROTO_IPV6
optname
:需设置的选项optval
:指针,指向存放选项值的缓冲区optlen
:optval 缓冲区长度optname可选项
选项名称 | 说明 | 数据类型 |
---|---|---|
SOL_SOCKET | ||
SO_BROADCAST | 允许发送广播数据 | int |
SO_DEBUG | 允许调试 | int |
SO_DONTROUTE | 不查找路由 | int |
SO_ERROR | 获得套接字错误 | int |
SO_KEEPALIVE | 保持连接 | int |
SO_LINGER | 延迟关闭连接 | struct linger |
SO_OOBINLINE | 带外数据放入正常数据流 | int |
SO_RCVBUF | 接收缓冲区大小 | int |
SO_SNDBUF | 发送缓冲区大小 | int |
SO_RCVLOWAT | 接收缓冲区下限 | int |
SO_SNDLOWAT | 发送缓冲区下限 | int |
SO_RCVTIMEO | 接收超时 | struct timeval |
SO_SNDTIMEO | 发送超时 | struct timeval |
SO_REUSEADDR | 允许重用本地地址和端口 | int |
SO_TYPE | 获得套接字类型 | int |
SO_BSDCOMPAT | 与 BSD 系统兼容 | int |
IPPROTO_IP | ||
IP_HDRINCL | 在数据包中包含 IP 首部 | int |
IP_OPTINOS | IP 首部选项 | int |
IP_TOS | 服务类型 | |
IP_TTL | 生存时间 | int |
IPPRO_TCP | ||
TCP_MAXSEG | TCP 最大数据段的大小 | int |
TCP_NODELAY | 不使用 Nagle 算法 | int |
实例7:使用SO_REUSEADDR实现重用本地地址和端口
解决了程序再次使用IP是被使用 Address already in use 的问题,就是 setsockopt 加上 SO_REUSEADDR
使用之前的服务器和客户端(实例3)
将封装的文件修改
int tcpInit(int *sfd, char *ip, char *port)
{
int socketFd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(socketFd, -1, "socket");
struct sockaddr_in serAddr;
bzero(&serAddr, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(atoi(port));
serAddr.sin_addr.s_addr = inet_addr(ip);
int ret;
int reuse = 1;
ret = setsockopt(socketFd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int)); -设置属性
ERROR_CHECK(ret, -1, "setsockopt");
ret = bind(socketFd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "bind");
listen(socketFd, 10);
*sfd = socketFd;
return 0;
}
就可以实现退出后重用同地址的效果
全部都必须要放在 bind
之前,另外通常是用于 UDP 的。
ESTABLISHED
状态下的 socket(一般由端口号和标志符区分)调用 closesocket
(一 般不会立即关闭而经历 TIME_WAIT
的过程)后想继续重用该 socket: setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)&reuse,sizeof(int));
send()
, recv()
过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限: int nNetTimeout=1000; -1 秒
- 发送时限
setsockopt(socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
- 接收时限
setsockopt(socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));
send()
的时候,返回的是实际发送出去的字节(同步)或发送到 socket 缓冲区的字节(异步),系统默认的 状态发送和接收一次为 8688 字节(约为 8.5K);在实际的过程中发送数据和接收数据量比较大,可以设置 socket 缓冲区,而避免了 send()
,recv()
不断的循环收发: - 接收缓冲区
int nRecvBuf=32*1024; - 设置为 32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
- 发送缓冲区
int nSendBuf=32*1024; - 设置为 32K
setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_SNDBUF,(char *)&nZero,sizeof(int));
recv()
完成上述功能(默认情况是将 socket 缓冲区的内容拷贝到系统缓冲区): int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_RCVBUF,(char *)&nZero,sizeof(int));
int bBroadcast= 1;
setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(int));
3.4. 单播、广播、组播(多播)(不常用)
多播广播是用于建立分步式系统:例如网络游戏、ICQ 聊天构建、远程视频会议系统的重要工具。 使用多播广播的程序和 UDP 的单播程序相似。区别在于多播广播程序使用特殊的 IP 地址。
对于单播而言,单播用于两个主机之间的端对端通信。
对于广播而言,广播用于一个主机对整个局域网上所有主机上的数据通信。
(广播只能用于客户机向 服务器广播,因为客户机要指明广播的 IP 地址“192.168.0.255”和广播的端口号。服务器端 bind
的时候, 绑定的端口号要跟广播的端口号是同一个。这样才能收到广播消息。)
对于多播而言,也称为 “组播” ,将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。单播和广播是两个极端,要么对一个主机进行通信,要么对整个局域网上的主机进行通信。实际情况下,经常需要对一组特定的主机进行通信,而不是整个局域网上的所有主机,这就是多播的用途。例如,我们通常所说的 讨论组。IPv4 多播地址采用 D 类 IP 地址确定多播的组。在 Internet 中,多播地址范围是从 224.0.0.0 到 234.255.255.255。(可穿透局域网)
多播的程序设计也要使用 setsockopt()
函数和 getsockopt()
函数来实现。其中对于 setsockopt
的第二个 参数 level 不再是 SOL_SOCKET
,而是 IPPROTO_IP
;而且第三个参数 optname
常见的选项有:
Optname | 含义 |
---|---|
IP_ADD_MEMBERSHIP | 在指定接口上加入组播组 |
IP_DROP_MEMBERSHIP | 退出组播组 |
选项 IP_ADD_MEMBERSHIP 和 IP_DROP_MEMBERSHIP 加入或者退出一个组播组
通过选项 IP_ADD_MEMBERSHIP 和 IP_DROP_MEMBERSHIP,对一个结构 struct ip_mreq
类型的变量进行控制。struct ip_mreq
原型如下:
struct ip_mreq
{
struct in_addr imr_multiaddr; -加入或者退出的多播组 IP 地址
struct in_addr imr_interface; -加入或者退出的网络接口 IP 地址,本机 IP
};
选项 IP_ADD_MEMBERSHIP 用于加入某个多播组,之后就可以向这个多播组发送数据或者从多播 组接收数据。此选项的值为 mreq
结构,成员 imr_multiaddr
是需要加入的多播组IP地址,成员 imr_interface
是本机需要加入多播组的网络接口 IP 地址。例如:
3.5. 什么是 DDos(SYN Flooding)攻击,如何防护
在探讨 SYN 攻击之前,我们先看看 linux 内核对 SYN 是怎么处理的:
利用上述特点。我们构造网络包,源地址随机构建,意味着当 Server 接到 SYN 包时,应答 ack 和 syn 时不会得到回应。在这种情况下, Server 端,内核就会维持一个很大的队列来管理这些半连接。 当半连接足够多的时候,就会导致新来的正常连接请求得不到响应, 也就是所谓的 DOS 攻击。 详细见下图所示:
SYN Flood 攻击防护手段
一般的防御措施就是就是减小 SYN+ACK 重传次数,增加半连接队列长度,启用 syn cookie。
不过在高强度攻击面前,调优 tcp_syn_retries 和 tcp_max_syn_backlog 并不能解决根本问题,更有效的防御手段
是激活 tcp_syncookies,在连接真正创建起来之前,它并不会立刻给请求分配数据区存储连接状态,而
是通过构建一个带签名的序号来屏蔽伪造请求。
(DDOS拒绝服务攻击)
下面的仅作了解
3.6. 描述符属性修改及文件描述符的传递
1. fcntl 函数
头文件:
#include
#include
#include
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, int arg);
int fcntl(int fd, int cmd, struct flock *lock);
作用:可以改变已打开的文件描述符性质,针对(文件)描述符提供控制
参数:
① 参数fd
是被参数 cmd 操作(如下面的描述)的描述符,代表要设置的文件描述符.
(针对 cmd 的值,fcntl 能够接受第三个参数 int arg )
② 参数cmd
代表打算操作的指令。
有以下几种情况:
F_DUPFD
用来查找大于或等于参数arg
的最小且仍未使用的文件描述符,并且复制参数fd
的文件描述符。执行成功则返回新复制的文件描述符。新描述符与fd共享同一文件表项,但是新描述符有它自己的一套文件描述符标志,其中FD_CLOEXEC
文件描述符标志被清除。如dup2()。F_GETFD
取得close-on-exec旗标。若此旗标的FD_CLOEXEC
位为0,代表在调用exec()
相关函数时文件将不会关闭。F_SETFD
设置close-on-exec 旗标。该旗标以参数arg
的FD_CLOEXEC
位决定。F_GETFL
取得文件描述符状态旗标,此旗标为 open()
的参数 flags
。F_SETFL
设置文件描述符状态旗标,参数arg为新旗标,但只允许O_APPEND、O_NONBLOCK和O_ASYNC位的改变,其他位的改变将不受影响。F_GETLK
取得文件锁定的状态。 ③ 参数lock
参数lock指针为flock 结构指针
定义如下:
struct flock
{
short int l_type;
short int l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
};
.
l_type
有三种状态:F_RDLCK
建立一个供读取用的锁定F_WRLCK
建立一个供写入用的锁定F_UNLCK
删除之前建立的锁定
.
l_whence
也有三种方式:SEEK_SET
以文件开头为锁定的起始位置。SEEK_CUR
以目前文件读写位置为锁定的起始位置SEEK_END
以文件结尾为锁定的起始位置。
.
l_start
表示相对l_whence
位置的偏移量,两者一起确定锁定区域的开始位置。
.
**l_len
**表示锁定区域的长度,如果为0表示从起点(由l_whence
和l_start
决定的开始位置)开始直到最大可能偏移量为止。即不管在后面增加多少数据都在锁的范围内。
.
返回值: 成功返回依赖于cmd的值,若有错误则返回-1,错误原因存于errno.
2. Socketpair 函数简介
int socketpair(int domain, int type, int protocol, int sv[2]);
作用 : 建立一对匿名的已经连接的套接字
前面 3 个参数参照 socket,domain 变为 AF_LOCAL,sv[2] , 放我们 fd[2]
3. Sendmsg 函数简介
ssize_t sendmsg (int s, const struct msghdr *msg, int flags);
作用 sendmsg
系统调用用于发送消息到另一个套接字
参数:
要在其上发送消息的套接口 s
信息头结构指针 msg
,这会控制函数调用的功能
可选的标记位参数 flags
。这与 send
或是 sendto
函数调用的标记参数相同。
返回值 : 函数的返回值为实际发送的字节数。否则,返回-1 表明发生了错误,而 errno 表明错误原因。
struct msghdr
结构体
结构定义如下:
struct msghdr {
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
size_t msg_iovlen;
void *msg_control;
size_t msg_controllen;
int msg_flags;
};
结构成员可以分为四组。
他们是:
成员 msg_name 与 msg_namelen
这些成员只有当我们的套接口是一个数据报套接口时才需要。
msg_name
成员指向我们要发送或是接收信息的套接口地址。
成员 msg_namelen
指明了这个套接口地址的长度。
( 当调用 recvmsg
时,msg_name
会指向一个将要接收的地址的接收区域。当调用 sendmsg
时,这会指向一个数据报将要发送到的目的地址。 )
( 注意,msg_name
定义为一个(void *)数据类型。我们并不需要将我们的套接口地址转换为(struct sockaddr *)
。 )
成员 msg_iov 与 msg_iovlen
这些成员指定了我们的 I/O 向量数组的位置以及他包含多少项。
msg_iov
成员指向一个 struct iovec 数组。我们将会回忆起 I/O 向量指向我们的缓冲区。
成员 msg_iov
指明了在我们的 I/O 向量数组中 有多少元素。
成员 msg_control 与 msg_controllen
这些成员指向了我们附属数据缓冲区并且表明了缓冲区大小。
msg_control
指向附属数据缓冲区
msg_controllen
指明了缓冲区大小。
这个通过 man cmsg 了解,或者通过下面的 cmsg 熟悉
4. Recvmsg
int recvmsg(int s, struct msghdr *msg, unsigned int flags);
参数:
要在其上接收信息的套接口 s
信息头结构指针 msg
,这会控制函数调用的操作。
可选标记位参数 flags
。
与 recv 或是 recvfrom 函数调用的标记参数相同。
这个函数的返回值为实际接收的字节数。否则,返回-1 表明发生了错误,而 errno 表明错误原因。
5. Writev
ssize_t writev(int fd, const struct iovec *iov, int iovcnt)
一次写入多个 buf 内容
struct iovec {
void iov_base; / Starting address /
size_t iov_len; / Number of bytes to transfer */
};
6.Readv
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
一次读取多个 buf 内容
7. Cmsg 用来设定我们的*msg_control 指针
size_t CMSG_LEN(size_t length);
返回结构体 cmsghdr 的大小,length 填入的是 cmsg_data[]的大小,我们填入的是描述符 fd,所以是 sizeof(int)
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);
struct cmsghdr {
socklen_t cmsg_len; /* data byte count, including he ader */
int cmsg_level; /* originating protocol */
int cmsg_type; /* protocol-specific type */
/* followed by unsigned char cmsg_data[]; */
};
CMSG_DATA 返回 cmsg_data 的起始地址,也就是通过它放入我们的文件描述符
*(int*)CMSG_DATA(cmsg)=fd;
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;