前言
研一的时候写过socket网络编程,研二这一年已经在用php写api都快把之前的基础知识忘干净了,这里回顾一下,主要也是项目里用到了,最近博客好杂乱啊,不过确实是到了关键时刻,各种复习加巩固准备9月份校招,顺便优美的完成手里的项目
概述
socket这个词可以有很多概念:
- 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通迅中的一个进程,“IP地址+端口号”就称为socket
- 在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就是唯一标识一个连接。socket本身有“插座”的意思,用来描述网络连接中一对一关系
- TCP/IP协议最早是BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API
预备知识
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按照内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:
先发出去的数据是低地址,后发出去的数据是高地址
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8
为了网络程序具有可移植性,使同样的c代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
#include
/*h表示host,n表示network, htonl表示将32位长整数从主机字节序转换为网络字节序*/
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
socket地址的数据类型以及相关函数
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPV4,IPV6,UNIX Domain Socket
ipv4地址的数据结构 struct sockaddr_in:
基于TCP协议的网络程序
TCP协议通迅流程:
服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等到服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回
数据传输过程:
建立连接后,TCP提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭管道一样,服务器的read()返回0,这样服务器知道客户端关闭了连接,也调用close()关闭连接
简单的TCP网络程序
先写一个服务器端的监听程序,作用是从客户端读取字符,接收到后告知客户端,作为补充,这里增加了通过fork()支持客户端的并发操作的实现:
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 1000
#define SERV_PORT 9931
void doprocessing(int sock, struct sockaddr_in cliaddr)
{
int n;
char str[INET_ADDRSTRLEN];
char *buf = (char *)malloc(MAXLINE);
memset(buf, '\0', MAXLINE);
while (1) {
n = read(sock, buf, MAXLINE);
if (n < 0) {
perror("Error reading from socket!");
return;
} else {
printf("Connect from %s:%d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
write(sock, "I got your message!", 20);
}
}
free(buf);
}
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
int listenfd, connfd, pid;
socklen_t cliaddr_len = sizeof(cliaddr);
// first call to socket() function
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
perror("Error opening socket!");
exit(-1);
}
// initialize socket structure
memset(&servaddr, 0, sizeof(struct sockaddr_in));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("222.31.79.131");
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
// start listening for the clients
listen(listenfd, 2000);
printf("王正一的server服务器开始等待客户端连接 ...\n");
while (1) {
// 三次握手完成后,服务器调用accept()接受客户端连接
// accept()参数:
// sockfd = listenfd, 服务器端监听的套接字描述符
// cliaddr = &cliaddr, 指向sockaddr结构体指针,客户端的地址信息
// addrlen = &cliaddr_len,确定客户端地址结构体的大小
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
pid = fork();
if (pid == -1) {
perror("call to fork");
exit(-1);
} else if (pid == 0) {
close(listenfd);
doprocessing(connfd, cliaddr);
exit(0);
} else {
close(connfd);
}
}
return 0;
}
运行状态:
在写一个客户端通过socket通信向服务器发送数据的代码:
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 80
#define SERV_PORT 9931
int main(int argc, char *argv[])
{
// 需要连接的服务器端socket套接字
// 客户端的socket套接字由系统自动分配
struct sockaddr_in servaddr;
char *buf = (char *)malloc(MAXLINE);
int servfd, n;
servfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("222.31.79.131"); // 传媒ip
servaddr.sin_port = htons(SERV_PORT);
// 客户端调用connect连接服务器端指定socket套接字
connect(servfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL) {
write(servfd, buf, strlen(buf));
n = read(servfd, buf, MAXLINE);
if (n == 0) {
perror("Tht other side has been closed.");
exit(-1);
} else {
printf("从服务器返回的信息为:%s\n", buf);
}
}
close(servfd);
return 0;
}
运行过程:
client发起请求:
server接受请求:
TCP三次握手及四次握手详细图解
相对于SOCKET开发者,TCP创建过程和链接拆除过程是由TCP/IP协议栈自动创建的,因此开发者并不需要控制这个过程,但是对于理解TCP底层运作机制,相当有帮助
TCP三次握手
所谓三次握手(Three-way Handshake),是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。
三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换TCP窗口大小信息,在socket编程中,客户端执行connect()时。将触发三次握手。
第一次握手:
客户端发送一个TCP的SYN标志字段为1的包指明客户打算连接的服务器的端口,以及初始序号X,保存在包头的序列号(sequence number)字段里
第二次握手:
服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将确认序列号置为X+1,并同时设置自己的发送序列号为Y
第三次握手
客户端再次发回确认包(ACK),SYN字段为0,ACK字段置为1,并把服务器端发过来的ACK的序号字段+1,放在确定字段中发送给对方,并且在数据段写ISN的+1
SYN攻击
在三次握手过程中,服务器发送SYN-ACK之后,收到客户端的ACK之前的TCP连称为半连接(half-open connect),此时,服务器处于SYN_RECV状态,当收到ACK包后,服务器转入ESTABLISHED状态
SYN攻击就是攻击客户端在短时间内伪造大量不存在的ip地址,向服务器不断发送syn包,服务器回复确认包,等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包长时间占用未连接队列,正常的syn请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪
syn攻击是一点典型的DDOS攻击,检查syn的攻击比较方便,利用netstat命令即可:
sudo netstat -nt | grep SYN_RECV
TCP四次挥手
问题
为什么建立连接协议是三次握手,而关闭连接确却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建立连接请求后,它可以把SYN和ACK(ACK起到应答作用,SYN起到同步作用)放在一个报文里发送。但关闭链接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发给了对方,所以你未必会马上关闭SOCKET连接,也就是你也需要告知对方你也不发数据了,重复上述两步过程,因此需要四步
TCP连接状态
如上图所示,通常情况下:一个正常的TCP连接,都会有三个阶段:
SYN(同步序列号,Synchronize Sequence Numbers)该标志仅在三次握手建立TCP连接时有效。表示一个新的TCP请求
ACK(确认编号,Acknowledgement Number)是对TCP请求的确认标志,同时提示对端系统已经接收所有数据
FIN(结束标志,FINISH),用来结束一个TCP会话,但对应端口仍处于开放状态,准备接收后续数据
LISTEN
首先,服务器端需要打开一个socket进行监听,状态为LISTEN。监听来自客户端的TCP连接
SYN_SENT
客户端通过应用程序调用connect进行active open。于是客户端tcp发送一个SYN以请求建立一个连接,之后状态置为SYN_SENT
SYN_RECV
服务器发出ACK确认客户端的SYN,同时自己向客户端发送一个SYN,同时状态置为SYN_RECV
ESTABLISHED
代表一个打开的连接,双方可以进行或已经在数据交互了
FIN_WAIT1
主动关闭端应用程序调用close,于是TCP发出FIN请求关闭连接,之后进行FIN_WAIT1状态
CLOSE_WAIT
被动关闭端TCP接到FIN请求后,就发送ack回复FIN请求,并且进入CLOSE_WAIT状态
FIN_WAIT2
主动关闭端接到ACK后,就进入FIN_WAIT_2
LAST_ACK
被动关闭端一段时间后,接收到文件结束符的应用程序将调用CLOASE关闭连接。
这导致它的TCP也发送一个FIN,等待对方的ACK,进入LAST_ACK
TIME_WAIT
在主动关闭端收到FIN后,TCP就发送ACK包,并进入TIME-WAIT状态。等待足够的时间以确保远程TCP接收到连接中断请求的确认
CLOSED
被动关闭端接收到ACK包后,就进入了close状态,连接结束