linux&C这两天学到了网络编程这一章,自己写了一个小的”服务器”和”客户端”程序,目的在于简单理解tcp/ip模型,以及要搭建一台简单服务器,服务器和客户端最基本的事情要干什么,这篇博客就这个小程序,也简单分析了自己对”TCP-三次握手”过程的理解。因为初学网络编程,说的不对的地方欢迎大家评论交流。
套接字:
套接字由4部分组成,服务器IP地址和客户端IP地址以及服务器端口号和客户端端口号,是客户端和服务器端传输数据确定线路的保证,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
首先我们来看服务器端的代码:
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#define MAX_QUEUE_LENGTH 1024
int main(int argc,char *argv[])
{
int sock_fd,conn_fd;
struct sockaddr_in serv_addr,conn_addr;
socklen_t conn_len;
pid_t pid1;
char recv_buf[128];
int ret;
//创建套接字
sock_fd = socket(AF_INET,SOCK_STREAM,0);
/*参数分别为(使用IPV4 tcp/ip协议,使用tcp流套接字,
通过前两个参数来确定使用的协议类型,默认为零)*/
//mZ服务器端的套接字进行初始化
memset(&serv_addr,0,sizeof(struct sockaddr_in));
//memset函数将serv_addr 用0进行初始化
serv_addr.sin_family = AF_INET;
/*设置地址类型:对于sin_family,表示用tcp/ip协议编程,
所以此值只能为AF_INEF*/
serv_addr.sin_port = htons(4507);
//设置端口号为4507
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//关于ANADDR——ANY这个宏的解释在下面有,也可以向下面一样设定指定IP
//inet_pton(AF_INET,"XXX.XXX.XXX.XXX",&serv_addr.sin_addr);
//当我只有一块网卡,一个IP,所以也只能设置为自己的ip了
//绑定套接字
bind(sock_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in));
/*绑定套接字的过程是将我前面创建的套接字与初始化的端口绑定起来, 因为socket只是创建了一个套接字,这个套接字将在哪个端口上工作, 并没有被指定,作为服务器,它的IP和端口一般是固定的,因此我们需要将刚才
初始化的端口和套接字绑定到一起,这时套接字已经完成服务器的ip和端口这一半*/
//将套接字转化成监听套接字
listen(sock_fd,MAX_QUEUE_LENGTH);
//第二个参数比较重要,它是我们服务器已完成套接字队列的长度,accept函数每次
//会从已完成的套接字队列中拿走一个即conn_fd,也就是监听套接字,注意:内核会维护两个队列,
//一个是已完成的套接字队列,一个是未完成的套接字队列,一个套接字完成的过程是在三次握手的
//过程中完成的,是由tcp/ip协议栈完成的
//接受客户端的请求
conn_len = sizeof(struct sockaddr_in);
while(1)
{
conn_fd = accept(sock_fd,(struct sockaddr *)&conn_addr,&conn_len);
//服务器会将客户端的连接消息先放在未完成的队列中,三次握手之后,就到已完成队列中
//conn_fd就是accept返回的叫作连接套接字,它的信息有服务器的端口和IP以及客户的端口和IP,
//监听套接字继续监听,对于新的处理,系统会重新开一个线程处理,自己也可以用进程
printf("accept a new connection,ip:%s\n",inet_ntoa(conn_addr.sin_addr));
pid1 = fork();
if(pid1 == 0)
ret = recv(conn_fd,recv_buf,sizeof(recv_buf),0);
recv_buf[ret-1] = '\0';
}
printf("%s\n",recv_buf);
}
注释中解释了套接字的初始化,建立,绑定,监听的过程,结束之后我们服务器端的工作就做好了,为了检验程序是否正确,运行程序之后我们可以用netstat命令查看我们服务器端是否开启监听模式
$ netstat -apt | grep LISTEN
可以看到4507端口的状态是LISTEN。
在说说INADDR_ANY的作用
INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。举个例子:你的电脑上如果有多块网卡就会有好几个IP,或者说你的服务器有多台主机也会有好多个IP,而如果你的IP会发生变化,比如变多或者变少,但是为了减少bind()时的麻烦,可以统一设定就在0.0.0.0这个地址上监听。所有的信息都会到这个地址上,如果你需要将服务器上的所有IP中的80端口监听,再将80端口绑定就OK了,我就了解了这么多。
然后再看我们的客户端代码:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<netinet/in.h>
int main(int argc,char *argv)
{
struct sockaddr_in serv_addr;
int conn_fd;
//同样我们需要在客户端也建立套接字
conn_fd = socket(AF_INET,SOCK_STREAM,0);
//初始化与服务器端匹配的信息
memset(&serv_addr,0,sizeof(struct sockaddr_in));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(4507);
inet_aton("192.168.20.144",&serv_addr.sin_addr);
//用于向服务器端发送连接请求,服务器的IP地址和端口号由参数serv_addr指定
connect(conn_fd,(struct sockaddr*)&serv_addr,sizeof(struct sockaddr_in));
close(conn_fd);
}
客户端有点简单啊,但是可以登录,这样就可以基本实现我们的客户端和服务器之间连接了,下面我们进入我们的主题:是不是都忘了我们的主题是——“三次握手“。
在这个实例中,三次握手发生在客户端执行connect函数的时候。三次握手的过程下面的图片可以显示清楚,我再举个例子
第一次 client->server SYN=1,seq = x
第二次 server->client SYN=1,ACK = 1,seq = y,ack = x+1
第三次 client->server ACK=1,seq = x+1
SYN:同步序号,当SYN=1,ACK=0时,表明这是一个连接请求。
ACK:当ACK=1时,确认号字段即ack才有效。
seq:范围:[0,2^32 - 1],表示发送的第一个字节的编号。
ack:确认号,当ack = n,表示到n-1为止的所有字节都已经被收到。
另外我用tcpdump命令抓了个包,可以分析下,不对的地方指出
$ tcpdump port 4507
我的客户端IP为192.168.122.172,服务器的IP为192.168.20.128。
第一次:Flag[S],表示这是一个请求连接,S是SYN字母的首字母,seq是将要发送的首字节的序列号,此时SYN = 1,ACK = 0。
第二次:收到SYN = 1的包,将ACK置为1,ack = 1588197944表示在此之前的字节我已经全部收到。
第三次:当再次收到SYN=1,ACK=1的包时,将ack = 1,表示确认连接。