目录
创建套接字后,使用setsockopt函数对绑定地址快速重用就不会出现这个情况!!!!
一、网络采用分层的思想:
1.每一层实现不同的功能,对上层的数据做透明传输
2.每一层向上层提供服务,同时使用下层提供的服务
二、各层典型的协议:
网络接口和物理层:
MAC地址:48位全球第一,网络设备的身份标识
ARP/RARP :
ARP: IP地址----->MAC地址
RARP: MAC地址--->IP地址
PPP协议:
拨号协议(GPRS/3G/4G)
网络层:
IP地址
IP: Internet protocol(分为IPV4和IPV6)
ICMP: Internet控制管理协议,ping命令属于ICMP
IGMP: Internet分组管理协议,广播、组播
传输层:
TCP: (Transfer Control protocol,传输控制协议) 提供面向连接的,一对一的可靠数据传输的协议,即数据无误、数据无丢失、数据无失序、数据无重复到达的通信。
UDP: (user Datagram Protocol, 用户数据报协议): 提供不可靠,无连接的尽力传输协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
SCTP: 是可靠传输,是TCP的增强版,它能实现多主机、多链路的通信。
应用层:
网页访问协议:HTTP/HTTPS
邮件发送接收协议: POP3(收)/SMTP(发) 、IMAP(可接收邮件的一部分)
FTP,
Telnet/SSH: 远程登录
嵌入式相关:
NTP: 网络时钟协议
SNMP: 简单网络管理协议(实现对网络设备集中式管理)
RTP/RTSP:用传输音视频的协议(安防监控)
三、 网络的封包和拆包
四、网络编程的预备知识
4.1 SOCKET
4.1.1 socket是一个应用编程的接口,它是一种特殊的文件描述符(对它执行IO的操作函数,比如,read(),write(),close()等操作函数)
4.1.2 socket代表着网络编程的一种资源
4.1.3 socket的类型:
流式套接字(SOCK_STREAM): 唯一对应着TCP
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。数据报套接字(SOCK_DGRAM): 唯一对应着UDP
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。原始套接字(SOCK_RAW):(对应着多个协议,发送穿透了传输层)
可以对较低层次协议如IP、ICMP直接访问。
4.2 IP地址
1.IP地址分为IPV4和IPV6
IPV4:采用32位的整数来表示
IPV6:采用了128位整数来表示
mobileIPV6: local IP(本地注册的IP),roam IP(漫游IP)
IPV4地址:
点分形式: 192.168.7.246
32位整数
特殊IP地址:
局域网IP: 192.XXX.XXX.XXX 10.XXX.XXX.XXX
广播IP: xxx.xxx.xxx.255, 255.255.255.255(全网广播)
组播IP: 224.XXX.XXX.XXX~239.xxx.xxx.xxx
4.3 端口号
16位的数字(1-65535)
众所周知端口: 1~1023(FTP: 21,SSH: 22, HTTP:80, HTTPS:469)
保留端口: 1024-5000(不建议使用)
可以使用的:5000~65535
TCP端口和UDP端口是相互独立的
网络里面的通信是由 IP地址+端口号 来决定
4.4 字节序
字节序是指不同的CPU访问内存中的多字节数据时候,存在大小端问题
如CPU访问的是字符串,则不存在大小端问题
一般来说:
X86/ARM: 小端
powerpc/mips, ARM作为路由器时:大端模式
网络传输的时候采用大端模式
IP地址转换函数:
in_addr_t inet_addr(const char *cp);
cp: 点分形式的IP地址,结果是32位整数(内部包含了字节序的转换,默认是网络字节序的模式)
特点: 1. 仅适应于IPV4
2. 当出错时,返回-1
3.此函数不能用于255.255.255.255的转换
inet_pton()/inet_ntop()
特点: 1.适应于IPV4和IPV6
2.能正确的处理255.255.255.255的转换问题
参数:
1.af: 地址协议族(AF_INET或AF_INET6)
2.src:是一个指针(填写点分形式的IP地址[主要指IPV4])
3.dst: 转换的结果给到dst
RETURN VALUE
inet_pton() returns 1 on success (network address was successfully con‐verted). 0 is returned if src does not contain a character string representing a valid network address in the specified address family. If af does not contain a valid address family, -1 is returned and errno is set to EAFNOSUPPORT.
五、TCP编程API
1.socket()函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int socket(int domain, int type, int protocol);
1.1参数
1.domain:
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_NETLINK Kernel user interface device netlink(7)
AF_PACKET Low level packet interface packet(7)
2.type:
SOCK_STREAM: 流式套接字 唯一对应于TCP
SOCK_DGRAM: 数据报套接字,唯一对应着UDP
SOCK_RAW: 原始套接字
3.protocol: 一般填0,原始套接字编程时需填充
1.2返回值:
RETURN VALUE
On success, a file descriptor for the new socket is returned. On
error, -1 is returned, and errno is set appropriately.
成功时返回文件描述符,出错时返回为-1
2.bind()函数
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
2.1 参数:
sockfd: 通过socket()函数拿到的fd
addr: struct sockaddr的结构体变量的地址
addrlen: 地址长度
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
如果是IPV6的编程,要使用struct sockddr_in6结构体(详细情况请参考man 7 ipv6),通常更通用的方法可以通过struct sockaddr_storage来编程
3.listen()函数:
把主动套接字变成被动套接字
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int listen(int sockfd, int backlog);
参数:
sockfd: 通过socket()函数拿到的fd
backlog: 同时允许几路客户端和服务器进行正在连接的过程(正在三次握手)
一般填5, 测试得知,ARM最大为8
内核中服务器的套接字fd会维护2个链表: 1. 正在三次握手的的客户端链表(数量=2*backlog+1) 2.已经建立好连接的客户端链表(已经完成3次握手分配好了newfd) |
比如:listen(fd, 5); //表示系统允许11(=2*5+1)个客户端同时进行三次握手
返回值:
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
4.accept():
阻塞等待客户端连接请求
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
4.1参数:
sockfd: 经过前面socket()创建并通过bind(),listen()设置过的fd
addr和addrlen: 获取连接过来的客户的信息
4.2 返回值:
RETURN VALUE
On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket. On
error, -1 is returned, and errno is set appropriately.
成功时返回已经建立好连接的新的newfd
5.客户端的连接函数 connect()
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
connect()函数和服务器的bind()函数写法类似:
5.1 参数:
sockfd: 通过socket()函数拿到的fd
addr: struct sockaddr的结构体变量的地址
addrlen: 地址长度
5.2 返回值:
RETURN VALUE
If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately.
6. 套接字的关闭close()
示例代码:
服务端:
#include "net.h"
#define flag 0
#if flag //多线程
void *cli_data_handle(void *arg);
#else //多进程
void cli_data_handle(void *arg);
#endif
void sig_child_handle(int signo)
{
if(SIGCHLD == signo)
{
waitpid(-1,NULL,WNOHANG);
}
}
int main(void)
{
int fd;
struct sockaddr_in sin;
signal(SIGCHLD,sig_child_handle);
//1.创建套接字
if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket");
exit(1);
}
//优化4:允许绑定地址快速重用
int b_reuse = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuse,sizeof(int));
memset(&sin,0,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVER_PORT);
//优化1:让服务器程序能绑定在任意的IP上
#if 0
//sin.sin_addr.s_addr = inet_addr(SERVER_IP);
sin.sin_addr.s_addr = htonl(INADDR_ANY);
#else
if(inet_pton(AF_INET,SERVER_IP,(void *)&sin.sin_addr) != 1)
{
perror("inet_pton");
exit(1);
}
#endif
//2.绑定
if(bind(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("bind");
exit(1);
}
//3.调用listen()把主动套接字变成被动套接字
if(listen(fd,5) < 0)
{
perror("listen");
exit(1);
}
printf("Server starting...OK!\n");
//4.阻塞等待客户端连接请求
int newfd;
//printf("newfd = %d\n",newfd);
#if flag
/*优化2:用多进程/多线程处理已经建立号连接的客户端数据*/
pthread_t tid;
while (1)
{
struct sockaddr_in cin;
socklen_t addrlen = sizeof(cin);
if((newfd = accept(fd,(struct sockaddr *)&cin,&addrlen)) < 0)
{
perror("accept");
exit(1);
}
char ipv4_addr[16];
//将ip地址的网络字节序转换为本地字节序
if(!inet_ntop(AF_INET,(void *)&cin.sin_addr,ipv4_addr,sizeof(cin)))
{
perror("inet_ntop");
exit(1);
}
//ntohs将端口号通过网络字节序转换为本地字节序
printf("Client(%s:%d) is connected!\n",ipv4_addr,ntohs(cin.sin_port));
pthread_create(&tid,NULL,cli_data_handle,(void *)&newfd);
}
#else //多进程
struct sockaddr_in cin;
socklen_t addrlen = sizeof(cin);
while (1)
{
pid_t pid = -1;
if((newfd = accept(fd,(struct sockaddr *)&cin,&addrlen)) < 0)
{
perror("accept");
break;
}
//创建一个子进程用于处理已建立连接的客户的交互数据
if((pid = fork()) < 0)
{
perror("fork");
break;
}
if (0 == pid) //子进程
{
close(fd);
char ipv4_addr[16];
//将ip地址的网络字节序转换为本地字节序
if(!inet_ntop(AF_INET,(void *)&cin.sin_addr,ipv4_addr,sizeof(cin)))
{
perror("inet_ntop");
exit(1);
}
//ntohs将端口号通过网络字节序转换为本地字节序
printf("Client(%s:%d) is connected!\n",ipv4_addr,ntohs(cin.sin_port));
cli_data_handle(&newfd);
return 0;
}
else // 父进程
{
close(newfd);
}
}
#endif
close(fd);
return 0;
}
#if flag
void *cli_data_handle(void *arg)
#else
void cli_data_handle(void *arg)
#endif
{
int newfd = *(int *)arg;
#if flag //多线程
printf("handler thread: newefd = %d\n",newfd);
#else //多进程
printf("Child handling process: newfd = %d\n",newfd);
#endif
int ret = -1;
char buf[BUFSIZ];
while (1)
{
memset(buf,0,sizeof(buf));
do{
ret = read(newfd,buf,BUFSIZ-1);
}while(ret<0 && EINTR == errno);
if (ret < 0)
{
perror("read");
exit(1);
}
if(!ret)//对方已经关闭
{
break;
}
printf("Receive data: %s\n",buf);
if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))//用户输入了quit字符
{
printf("Client(fd=%d) is exiting!\n",newfd);
break;
}
}
close(newfd);
}
客户端:
#include "net.h"
void usage(char *s)
{
printf("\n%s serv_ip serv_port",s);
printf("\n\t serv_ip: server ip address");
printf("\n\t serv_port: server port(>5000)\n\n");
}
int main(int argc,char* argv[])
{
int fd;
int port;
struct sockaddr_in sin;
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
//1.创建套接字
if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket");
exit(1);
}
port = atoi(argv[2]);
if(port < 5000)
{
usage(argv[0]);
exit(1);
}
//2.连接服务器
memset(&sin,0,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(port);
#if 0
sin.sin_addr.s_addr = inet_addr(SERVER_IP);
#else
if(inet_pton(AF_INET,SERVER_IP,(void *)&sin.sin_addr) != 1)
{
perror("inet_pton");
exit(1);
}
#endif
if(connect(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("connect");
exit(1);
}
printf ("Client staring...OK!\n");
//3.读写数据
char buf[BUFSIZ];
int ret = -1;
while (1)
{
memset(buf,0,sizeof(buf));
if(fgets(buf,BUFSIZ-1,stdin) == NULL)
{
continue;
}
do
{
ret = write(fd,buf,strlen(buf));
} while (ret < 0 && EINTR == errno);
if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))//用户输入了quit字符
{
printf("Client is exiting!\n");
break;
}
}
//4.关闭套接字
close(fd);
//return 0;
}
运行结果:
重大知识点:(项目代码改进需要)
网络属性设置:
问题描述:在tcp连接下,如果服务器主动关闭连接(比如ctrl+c结束服务器进程),那么由于服务器这边会出现time_wait状态,所以不能立即重新启动服务器进程。
通过netstat可以查看端口运行状态
-a (all)显示所有选项,默认不显示LISTEN相关
-t (tcp)仅显示tcp相关选项
-u (udp)仅显示udp相关选项
-n 拒绝显示别名,能显示数字的全部转化成数字。
-l 仅列出有在 Listen (监听) 的服務状态-p 显示建立相关链接的程序名
-r 显示路由信息,路由表
-e 显示扩展信息,例如uid等
-s 按各个协议进行统计
-c 每隔一个固定时间,执行该netstat命令。提示:LISTEN和LISTENING的状态只有用-a或者-l才能看到
服务端和客户端建立连接后,如果先断开服务端,再启动服务端的时候会出现如下bind错误:
服务端代码优化 :
创建套接字后,使用setsockopt函数对绑定地址快速重用就不会出现这个情况!!!!
优化后重启服务器:
通过netstat命令查看这个端口上还是有time_wait的出现:
这时的time_wait是上次连接中与该地址和端口绑定的socket出现了time_wait状态,但是采用了端口复用后,可以多个socket和这个端口进行绑定,所以新的一次的连接使用的是新创建的socket和该端口进行绑定,并不会受到上一个socket处于time_wait的影响。
虽然可以立即启动服务器,但是对于高并发模式下的服务器在短时间内也是不能使用已经处于time_wait状态的socket的,要解决这样的问题就要用其它的方法(比如通过设置内核参数避免出现time_wait状态)。 上述中服务器能够立即重启是因为使用了新的socket和端口进行了绑定,而上次已经断开连接的socket还是在处于time_wait中的,这个socket在短时间内是不能再分配出去继续使用的。
补充send()/recv()函数API
网络接受数据:recv()/read()
六、UDP编程API
发送数据sendto() :
接受数据recvfrom():
示例代码:
服务端:
#include "net.h"
int main(void)
{
int fd;
struct sockaddr_in sin;
if((fd = socket(AF_INET,SOCK_DGRAM,0)) < 0)
{
perror("socket");
exit(1);
}
//允许绑定地址快速重用(好像不加这个也可以)
int b_reuse = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuse,sizeof(int));
memset(&sin,0,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVER_PORT);
sin.sin_addr.s_addr = inet_addr(SERVER_IP);
if(bind(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("bind");
exit(1);
}
char buf[BUFSIZ];
struct sockaddr_in cin;
socklen_t addrlen = sizeof(cin);
while (1)
{
memset(buf,0,sizeof(buf));
if(recvfrom(fd,buf,BUFSIZ-1,0,(struct sockaddr *)&cin,&addrlen) < 0)
{
perror("recvfrom");
exit(1);
}
char ipv4_addr[16];
if (!inet_ntop(AF_INET,(void *)&cin.sin_addr,ipv4_addr,sizeof(cin)))
{
perror("inet_ntop");
exit(1);
}
printf("Recvied from(%s:%d),data:%s\n",ipv4_addr,ntohs(cin.sin_port),buf);
}
close(fd);
return 0;
}
客户端:
#include "net.h"
int main(void)
{
int fd;
struct sockaddr_in sin;
if((fd = socket(AF_INET,SOCK_DGRAM,0)) < 0)
{
perror("socket");
exit(1);
}
memset(&sin,0,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVER_PORT);
sin.sin_addr.s_addr = inet_addr(SERVER_IP);
char buf[BUFSIZ];
while (1)
{
memset(buf,0,sizeof(buf));
if(fgets(buf,BUFSIZ-1,stdin) == NULL)
{
perror("fgets");
continue;
}
if(sendto(fd,buf,strlen(buf),0,(struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("sendto");
exit(1);
}
}
close(fd);
return 0;
}
头文件:
#ifndef __NET_H_
#define __NET_H_
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define SERVER_PORT 6780
#define SERVER_IP "192.168.1.66"
#endif
运行结果:
udp可以实现多个客户端对一个服务器
七、TCP、IP协议原理
TCP是一种面向连接,可靠的数据传输
TCP的可靠传输:通过确认和重发机制
- TCP把所有要发送的数据进行编号(每一个字节用一个号)
- 发送时从当前数据位置,发送window大小的数据
三次握手、四次挥手
服务端启动./client 客户端启动./server
整个流程:
wireshark工作原理:
使用wireshark软件进行抓包:
可以看到三次握手由客户端发起,完全符合下图流程
第一次握手传输层报文:SYN = 1,Seq = 0
第二次握手传输层报文:SYN = 1,ACK = 1,Seq = 0,ack = 1
第三次握手传输层报文:ACK = 1,seq = 1,ack = 1
各字段在TCP三次握手中的作用:
SYN:用于建立连接。
ACK:用于确定收到了请求。
seq:发送自己的数据。
ack:发送接收到的对方的数据。
上图为四次挥手:
第一次挥手传输报文:FIN = 1,ACK = 1,seq = 12,ack = 14(怀疑老师讲错了,他的意思是FIN = m = 12)
第二次挥手传输报文:FIN = 1,ACK = 1,seq = 14,ack = 13 (怀疑老师讲错了,他的意思是ACK = m+1 = 13,FIN = n=14)(其中通过wireshark看到这里包含了两次挥手)
第三次挥手传输报文:ACK = 1,seq = 13,ack = 15(怀疑老师讲错了,他的意思是ACK = n+1 = 15)
注意事项:
三次握手的连接 必须由客户端发起(四次挥手客户端和服务器都可以发起)
重点学习文章:活久见!TCP两次挥手,你见过吗?那四次握手呢?-优快云博客
八、unix域套接字(unix domain)
用于本地进程间的通信
创建套接字时使用本地协议PF_UNIX(或PF_LOCAL)。
socket(AF_LOCAL,SOCK_STREAM,0)
socket(AF_LOCAL,SOCK_DGRAM,0)
分为流式套接字和用户数据报套接字
进程间通信:
1、进程间的数据共享:
管道、消息队列、共享内存、unix域套接字
易用性:消息队列 > unix域套接字 > 管道 > 共享内存(经常要和信号量一起用)
效率:共享内存 > unix域套接字 > 管道 > 消息队列
常用:共享内存、unix域套接字
2、异步通信:
信号
3、同步和互斥(做资源保护)
信号量
示例代码
服务端:
#include "net.h"
int main(void)
{
int fd;
struct sockaddr_un sun;
if((fd = socket(AF_UNIX,SOCK_STREAM,0)) < 0)
{
perror("socket");
exit(1);
}
//允许绑定地址快速重用
int b_reuse = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuse,sizeof(int));
memset(&sun,0,sizeof(sun));
sun.sun_family = AF_UNIX;
//如果unix所指向的文件存在,则删除
if(!access(UNIX_DOMAIN_FILE,F_OK))
{
unlink(UNIX_DOMAIN_FILE);
}
strncpy(sun.sun_path,UNIX_DOMAIN_FILE,strlen(UNIX_DOMAIN_FILE));
if(bind(fd,(struct sockaddr *)&sun,sizeof(sun)) < 0)
{
perror("bind");
exit(1);
}
if(listen(fd,5) < 0)
{
perror("listen");
exit(1);
}
printf("server starting ...... OK\n");
int newfd;
if((newfd = accept(fd,NULL,NULL)) < 0)
{
perror("accept");
exit(1);
}
int ret = -1;
char buf[BUFSIZ];
while (1)
{
memset(buf,0,sizeof(buf));
// read(newfd,buf,BUFSIZ-1);
// printf("receive data: %s\n",buf);
do{
ret = read(newfd,buf,BUFSIZ-1);
}while(ret<0 && EINTR == errno);
if (ret < 0)
{
perror("read");
exit(1);
}
if(!ret)//对方已经关闭
{
break;
}
printf("Receive data: %s\n",buf);
}
close(newfd);
close(fd);
return 0;
}
客户端:
#include "net.h"
int main(void)
{
int fd;
struct sockaddr_un sun;
if((fd = socket(AF_UNIX,SOCK_STREAM,0)) < 0)
{
perror("socket");
exit(1);
}
memset(&sun,0,sizeof(sun));
sun.sun_family = AF_UNIX;
//确保UNIX_DOMAIN_FILE存在可写
if((access(UNIX_DOMAIN_FILE,F_OK|W_OK)) < 0)
{
exit(1);
}
strncpy(sun.sun_path,UNIX_DOMAIN_FILE,strlen(UNIX_DOMAIN_FILE));
if(connect(fd,(struct sockaddr *)&sun,sizeof(sun)) < 0)
{
perror("connect");
exit(1);
}
printf("client starting ok\n");
char buf[BUFSIZ];
while (1)
{
memset(buf,0,sizeof(buf));
if(fgets(buf,BUFSIZ-1,stdin) == NULL)
{
continue;
}
write(fd,buf,strlen(buf));
}
return 0;
}
net.h文件
#ifndef __NET_H_
#define __NET_H_
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define UNIX_DOMAIN_FILE "./my_domain_file.txt"
#endif
运行结果: