目录
网络编程中的基本概念
网络通信是网络上两个主机间进程通信,一个叫客户端,一个叫服务端,并且永远是客户端向服务端发起请求。
IP地址:
IP地址是在IP协议中, 用来标识网络中不同主机的地址。
IP协议有两个版本, IPv4和IPv6,对于IPv4来说, IP地址是⼀个4字节, 32位的整数,对于IPv6来说,IP地址是一个128字节的地址(不向下兼容)。
我们通常也使⽤用 "点分十进制" 的字符串表示IP地址, 例如 192.168.0.1 ,用点分割的每⼀个数字表示⼀个字节, 范围是 0 - 255。
端口号:
两个字节16位的无符号整数,在主机上用于表示一个进程,告诉操作系统,当前的数据交给哪一个进程来处理,端口号是传输层协议的内容。
一个进程可以使用多个端口号,但一个端口号只能被一个进程使用。
IP地址 + 端口号能够标识网络上的某⼀台主机的某⼀个进程。
TCP协议:(Transmission Control Protocol )传输控制协议,传输层协议
数据的可靠传输,有连接。
面向字节流数据传输,收发数据比较灵活,数据无明显边界,容易造成TCP粘包问题,发送和接受时要注意。
TCP可以保证数据的可靠传输,但是因为要保证数据的可靠传输,因此牺牲了很多的性能,常用于对数据安全性能比较高的场景。
UDP协议:(User Datagram Protocol )用户数据报协议,传输层协议
数据的不可靠传输,无连接。
面向数据报数据传输,数据发送时有最大长度限制,接收时一条一条接受。
UDP数据传输速度快,实时性高,常用于传输音乐、视频等对数据的完整性要求不是很高,但对实时性要求较高的场景。
网络字节序:
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址数据,后发出的数据是高地址数据。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址存放高字节数据。
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
h表⽰示host,n表⽰示network,l表⽰示32位⻓长整数,s表⽰示16位短整数;
例如htonl表⽰示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送;
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
socket编程接口(API)
以上函数接口在具体使用时作详细解释,这里先对以上函数中多次出现的 sockaddr 结构做个说明
socket API是⼀层抽象的网络编程接口,适⽤用于各种底层网络协议,如IPv4、IPv6,以及UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同。
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址.;
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容;
socket API可以都用struct sockaddr *类型表示, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
sockaddr结构:
sockaddr_in结构:
其中的in_addr⽤来表示一个IPv4的IP地址. 其实就是⼀一个32位的整数。
简单的UDP网络程序
这里我实现的是一个服务端和客户端能进行聊天的网络程序,程序中对API接口介绍的非常详细。
服务端程序:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<error.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
//一:创建套接字
//#include <sys/types.h>
//#include <sys/socket.h>
//int socket(int domain, int type, int protocol);
//socket打开一个网络通信窗口,如果成功的话返回一个文件描述符,应用程序就可以像读写文件
//那样用read/write在网络上发送数据
// domain:地址域
// AF_INET:IPv4 Internet protocols
// AF_INET6:IPv6 Internet protocols
// type:套接字类型
// SOCK_DGRAM 数据报套接字
// SOCK_STREAM 流式套接字
// protocol:协议类型
// 默认0时,数据报套接字默认udp协议,流式套接字默认tcp协议
//
// 返回值:套接字描述符,失败返回-1
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
perror("socket");
return -1;
}
//二:为socket绑定地址信息(确定socket能够操作缓冲区中的哪些数据)
//int bind(int sockfd, struct sockaddr *addr,socklen_t addrlen);
//
// sockfd:套接字描述符
// addr:要绑定的地址信息
// addrlen:地址信息的长度
// 返回值:0,失败时返回-1
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(9000);
local.sin_addr.s_addr = inet_addr("192.168.206.130");
if(bind(sockfd,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
close(sockfd);
return -1;
}
//接收数据
//#include <sys/types.h>
//#include <sys/socket.h>
//ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
// struct sockaddr *src_addr, socklen_t *addrlen);
// sockfd:套接字描述符
// buf:用于存储接受的数据
// len:想要接受的数据长度
// flags:默认0,如果缓冲区没有数据,那么就阻塞等待
// src_addr:用于确定数据的发送端地址信息
// addrlen:地址信息的长度
//
// 返回值:实际接受的数据长度,失败返回-1
char buf[1024];
struct sockaddr_in client;
while(1){
//buf[1024] = {0}; //记住不能这样搞
memset(buf,0,1024);
socklen_t len = sizeof(client);
if(recvfrom(sockfd,buf,sizeof(buf)-1,0, (struct sockaddr*)&client,len) < 0)
{
perror("recvfrom");
close(sockfd);
return -1;
}
printf("client[%s:%d],say:%s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
//发送数据
// #include <sys/types.h>
// #include <sys/socket.h>
//ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
//ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
// const struct sockaddr *dest_addr, socklen_t addrlen);
// sockfd:套接字描述符(发送数据时根据这个socket绑定的地址来发送)
// buf:要发送的数据
// len:发送数据的长度
// flags:默认0,阻塞式发送
// dest_addr:对端地址
// addrlen:地址信息的长度
//
// 返回值:实际发送数据的长度,失败返回-1
if(sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&client,sizeof(client)) < 0)
{
perror("sendto");
close(sockfd);
return -1;
}
}
close(sockfd);
return 0;
}
客户端程序:
#include<stdlib.h>
#include<unistd.h>
#include<stdlib.h>
#include<error.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
//创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
perror("socket");
return -1;
}
//为套接字绑定地址信息
//客户端程序中,通常我们不推荐绑定地址信息,因为手动绑定有可能会因特殊情况失败,
//客户端发送数据的时候,只要数据能发送成功就可以了,我们不关心具体的地址和
//所用的端口号,客户端程序中我们不手动绑定地址,发送数据时,操作系统检测到
//socket没有绑定地址信息,会自动为socket绑定地址和合适的端口,这种绑定方式一般不会出错。
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port =htons (9000);
server.sin_addr.s_addr = inet_addr("192.168.206.130");
char buf[1024];
while(1){
//发送数据
if(sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&server,sizeof(server)) < 0)
{
perror("sendto");
close(sockfd);
return -1;
}
//接收数据
memset(buf,0,1024);
socklen_t len = sizeof(server);
if(recvfrom(sockfd,buf,sizeof(buf)-1,0, (struct sockaddr*)&server,&len) < 0)
{
perror("recvfrom");
close(sockfd);
return -1;
}
printf("client[%s:%d],say:%s\n",inet_ntoa(server.sin_addr),ntohs(server.sin_port),buf);
}
close(sockfd);
return 0;
}
基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr,sin_addr表⽰示32位的IP地址,但是我们通常⽤用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示和in_addr表示之间进行相互转换
#include <sys/socket.h>
#include <netinet/in.h>#include <arpa/inet.h>
//字符串转in_addr的函数
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
//in_addr转字符串的函数
char *inet_ntoa(struct in_addr in);
inet_ntoa这个函数返回了一个char*很显然是这个函数自己在内部申请了一块内存来保存ip的结果,man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进⾏行释放. 因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调⽤用时的结果会覆盖掉上一次的结果。
简单的TCP网络程序
这里实现的是一个服务端和客户端能进行网络通信的程序。
服务端程序:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
int main()
{
//创建socket
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket");
return -1;
}
//为socket绑定地址信息
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(9000);
server.sin_addr.s_addr = inet_addr(" 192.168.206.130 ");
//网络地址可设为INADDR_ANY, 这个宏表⽰示本地的任意IP地址,因为服务器可能有多个网卡,每个
//网卡也可能绑定多个IP地址, 这样设置可以在所有的IP地址上监听,直到与客户端建立了连
//接时才确定下来到底用哪个IP地址
//服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址
//和端口号后就可以向服务器发起连接,服务器需要调⽤用bind绑定一个固定的网络地址和端口号;
//bind()的作用是将参数sockfd和addr绑定在⼀一起, 使sockfd这个用于网络通讯的文件描述符
//监听addr所描述的地址和端口号
//struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,
//而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
if(bind(sockfd,(struct sockaddr*)&server,sizeof(server)) < 0)
{
perror("bind");
close(sockfd);
return -1;
}
//开始监听
//#include <sys/types.h>
//#include <sys/socket.h>
//int listen(int sockfd, int backlog);
// sockfd:socket描述符
// backlog:最大的同时并发连接数
// 返回值:0,失败返回-1
if(listen(sockfd,5) < 0)
{
perror("listen");
close(socket);
return -1;
}
while(1){
//获取连接成功的socket
//int accept(int sockfd, struct sockaddr *addr,socklen_t *addrlen);
//三次握手完成后, 服务器调用accept()接受连接,
// sockfd:socket描述符
// addr:新建立连接的客户端地址信息
// addrlen:地址信息的长度
// 返回值:新建立的socket描述符,失败返回-1
//accept函数是一个阻塞性函数,连接成功队列中如果没有连接成功的socket,那么就一直等待直
//到有新的客户端连接(如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客
//户端连接上来)
int new_sockfd;
struct sockaddr_in client;
socklen_t len = sizeof(client);
new_sockfd = accept(sockfd,(struct sockaddr*)&client,&len);
if(new_sockfd < 0 )
{
perror("accept");
close(sockfd);
return -1;
}
printf("new connit->[%s:%d]",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
while(1){
//接受数据
// #include <sys/types.h>
//#include <sys/socket.h>
//ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
// struct sockaddr *src_addr, socklen_t *addrlen);
//ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// sockfd:新建立连接成功的socket描述符
// buf:用于存储接受的数据
// len:用于指定接收数据的长度
// flags:默认0,阻塞式接受数据
//
// 返回值:>0:实际接受数据的长度,=0:对端关闭连接,<0:出错
char buf[1024] = {0};
ssize_t len = recv(new_sockfd,buf,sizeof(buf)-1,0);
if(len < 0)
{
perror("recv");
close(new_sockfd);
continue;
}else if(len == 0)
{
perror("recv:len==0");
close(new_sockfd);
continue;
}else
{
printf("client:[%s:%d] say:%s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
}
//发送数据
memset(buf,0,1024);
scanf("%s",buf);
send(new_sockfd,buf,strlen(buf),0);
}
}
close(sockfd);
return 0;
}
客户端和服务端连接成功(完成三次握手)之后,连接成功的socket被放在一个称为连接成功队列的队列中,accept函数正是从这个队列中获取连接成功socket的,并且最多允许有backlog(最大同时并发连接数)个客户端处于连接等待状态,如果接收到更多的连接请求,将此时的socket(半连接状态的socket)放到一个被称为未完全连接成功的队列中,当在连接成功队列中有位置时,操作系统将未完全连接成功队列中的结点移到成功连接队列中。
sockfd和new_sockfd的区别:
sockfd:所有的请求数据都发送到这个sockfd描述的缓冲区,然后进行处理(会为这个新连接的客户端新建一个socket),这个sockfd描述的缓冲区接受的数据全是连接请求(ip、port);
new_sockfd:连接建立成功之后,new_sockfd描述新建立的socket,往后这个客户端发送的数据全在new_sockfd描述的缓冲区中。
客户端程序:
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
int main()
{
//创建socket
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd <0)
{
perror(socket);
return-1;
}
//绑定地址信息(但是客户端程序不推荐绑定地址信息)
//向服务端发起连接请求
//#include <sys/types.h>
//#include <sys/socket.h>
//int connect(int sockfd, const struct sockaddr *addr,
// socklen_t addrlen);
// sockfd:socket描述符
// addr:要连接的服务端地址
// addrlen:地址信息成都
//
// 返回值:0,失败返回-1
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(9000);
server.sin_addr.s_addr = inet_addr("192,168,206,130");
if(connect(sockfd,(struct socksddr*)&server,sizeof(server)) < 0)
{
perror(connect);
close(sockfd);
return -1;
}
char buf[1024] = {0};
while(1){
//发送数据
scanf("%s",buf);
send(sockfd,buf,strlen(buf),0);
//接收数据
memset(buf,0,1024);
ssize_t len = recv(sockfd,buf,sizeof(buf)-1,0);
if(len < 0)
{
perror(recv);
close(sockfd);
return -1;
}else if(len == 0)
{
perror(recv);
close(sockfd);
return -1;
}else
{
printf("[%s:%d] say:%s",inet_ntoa(server.sin_addr),ntohs(server.sin_port),buf);
}
}
close(sockfd);
return 0;
}
客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号,如果在同⼀台机器上启动多个客户端, 就会出现端口号被占用导致不能正确建立连接;
服务器也不是必须调用bind(), 但如果服务器不调⽤用bind(), 内核会自动给服务器分配监听端口, 每次启动服务器时端口号都不⼀一样, 客户端要连接服务器就会遇到⿇麻烦。
TCP连接管理
syn泛洪攻击:客户端发送请求,服务端回复后,客户端不再回复(对应阶段:三次握手建立连接过程)。
建⽴立连接后,TCP协议提供全双工的通信服务,所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由⼀方来写数据。
当客户端处于TIME_WAIT状态时,等待大概两个MSL时间后,关闭文件处于CLOSE状态。