一、整体过程图解
二、Socket编程模型细节
客户端和服务器能在网络中通信,那必须得使用Socket编程,它是进程间通信里比较特别的方式,特别之处在于它是可以跨主机间通信。
创建Socket 的时候,可以指定网络层使用的是IPv4还是IPv6,传输层使用的是TCP还是UDP。服务器的程序要先跑起来,然后等待客户端的连接和数据,我们先来看看服务端的Socket编程过程是怎样的。
服务端首先调用socket()函数,创建网络协议为IPv4,以及传输协议为TCP的Socket。
接着调用bind()函数,给这个Socket绑定一个IP地址和端口,绑定这两个的目的是什么?
- 绑定端口的目的:当内核收到TCP报文,通过TCP头里面的端口号,来找到我们的应用程序,然后把数据传递给我们。
- 绑定IP地址的目的:一台机器是可以有多个网卡的,每个网卡都有对应的IP地址,当绑定一个网卡时,内核在收到该网卡上的包,才会发给我们;
绑定完IP地址和端口后,就可以调用listen()函数进行监听,此时对应TCP状态图中的listen,如果我们要判定服务器中一个网络程序有没有启动,可以通过netstat命令查看对应的端口号是否有被监听。
服务端进入了监听状态后,通过调用accept()函数,来从内核获取客户端的连接,如果没有客户端连接,则会阻塞等待客户端连接的到来。
那客户端是怎么发起连接的呢?客户端在创建好Socket后,调用connect()函数发起连接,该函数的参数要指明服务端的IP地址和端口号,然后万众期待的TCP三次握手就开始了。
在TCP连接的过程中,服务器的内核实际上为每个Socket维护了两个队列:
- 一个是「还没完全建立」连接的队列,称为TCP半连接队列,这个队列都是没有完成三次握手的连接,此时服务端处于syn_rcvd 的状态;
- 一个是「已经建立」连接的队列,称为TCP全连接队列,这个队列都是完成了三次握手的连接,此时服务端处于established 状态;
当TCP全连接队列不为空后,服务端的accept()函数,就会从内核中的TCP全连接队列里拿出一个已经完成连接的Socket返回应用程序,后续数据传输都用这个Socket。
注意,监听的Socket和真正用来传数据的Socket是两个:
- 一个叫作监听Socket;
- 一个叫作已连接Socket;
连接建立后,客户端和服务端就开始相互传输数据了,双方都可以通过read()和write()函数来读写数据。
三、服务端、客户端代码供参考
服务器代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/socket.h>
#include<netinet/ip.h>
#include<arpa/inet.h> #define SERROPT 8000
#define SERIP "192.168.111.110"
int main(int argc, char* argv[])
{//创建一个套接字 socket参数1.协议类型 2.流式套接字 3.传0(默认TCP协议)int lfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in seraddr, cliaddr;seraddr.sin_family = AF_INET;seraddr.sin_port = htons(SERROPT); //8000->16415seraddr.sin_addr.s_addr=INADDR_ANY;//seraddr.sin_addr.s_addr = 2171971776; //192.168.117.129->2171971776//inet_pton(AF_INET, SERIP ,(void*)&seraddr.sin_addr.s_addr);//本地ip转网络ip函数//参数1.要绑定文件描述符 2.结构体包括(协议类型、端口号、地址)3.长度bind (lfd, (struct sockaddr*)&seraddr, sizeof(seraddr));listen(lfd,64);//socklen_t clilen = sizeof(cliaddr);int cfd = accept(lfd , (struct sockaddr*)&cliaddr, &clilen);//第三个套接字 //accept是在全连接队列中取出连接 如果多次连接 全连接里没有连接可用 会发生阻塞char buf[1024];while(1){int rr = read(cfd, buf, sizeof(buf));//读 读缓冲write(STDERR_FILENO, buf, rr); //写到标准输出上write(cfd, buf, rr); //反射回去证明客户端已经接收到了}return 0;
}
客户端代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/socket.h>
#include<netinet/ip.h>
#include<arpa/inet.h> #define SERROPT 8000
#define SERIP "192.168.111.110"
#define CLIPORT 8001
#define CLIIP "192.168.111.110"
int main(int argc, char* argv[])
{//创建一个套接字 int cfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in seraddr, cliaddr; //定义两个结构体cliaddr.sin_family = AF_INET;cliaddr.sin_port = htons(CLIPORT); inet_pton(AF_INET, CLIIP ,(void*)&cliaddr.sin_addr.s_addr);bind (cfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr)); //客户端绑定//seraddr.sin_family = AF_INET;seraddr.sin_port = htons(SERROPT); inet_pton(AF_INET, SERIP ,(void*)&seraddr.sin_addr.s_addr);connect(cfd, (struct sockaddr*)&seraddr, sizeof(seraddr));//连接char buf[1024];while(1){int rr = read(0, buf, sizeof(buf)); //等待输入write(cfd, buf, rr); rr = read(cfd, buf, sizeof(buf));write(1, buf, rr); //反射回去证明已经接收到了}return 0;
}
端口随便设定合理就行,IP地址(我这里随便写的IP)查看自己虚拟机IP(通过命令:ifconfig)查看,不要忘记更改,先启动服务器、再启动客户端,这样一个简单的Socket编程模型就搭建好了。
基于Linux一切皆文件的理念,在内核中Socket也是以「文件」的形式存在的,也是有对应的文件描述符。