socket 这个英文单词的原意是“插口”“插槽”, 在网络编程中,它的意思是可以通过插口接入的方式,快速完成网络连接和数据收发。你可以把它想象成现实世界的电源插口,或者是早期上网需要的网络插槽,所以 socket 也可以看做是对物理世界的直接映射。
在Linux中socket是一种文件类型,伪文件,不占用存储空间,可进行IO操作,可间接看做文件描述符使用。
首先,我们来看一张图
这张图表达的是网络编程中,客户端和服务器工作的核心逻辑。
我们先从右侧的服务器端开始看,因为在客户端发起连接请求之前,服务器端必须初始化好。右侧的图显示的是服务器端初始化的过程,首先初始化 socket,之后服务器端需要执行 bind 函数,将自己的服务能力绑定在一个众所周知的地址和端口上,紧接着,服务器端执行 listen 操作,将原先的 socket 转化为服务端的 socket,服务端最后阻塞在 accept 上等待客户端请求的到来。
此时,服务器端已经准备就绪。客户端需要先初始化 socket,再执行 connect 向服务器端的地址和端口发起连接请求,这里的地址和端口必须是客户端预先知晓的。这个过程,就是著名的TCP 三次握手。
一旦三次握手完成,客户端和服务器端建立连接,就进入了数据传输过程。
具体来说,客户端进程向操作系统内核发起 write 字节流写操作,内核协议栈将字节流通过网络设备传输到服务器端,服务器端从内核得到信息,将字节流从内核读入到进程中,并开始业务逻辑的处理,完成之后,服务器端再将得到的结果以同样的方式写给客户端。可以看到,一旦连接建立,数据的传输就不再是单向的,而是双向的,这也是 TCP 的一个显著特性。
讲这幅图的真正用意在于引入 socket 的概念,请注意,以上所有的操作,都是通过 socket 来完成的。无论是客户端的 connect,还是服务端的 accept,或者 read/write 操作等,socket 是我们用来建立连接,传输数据的唯一途径。
在极客时间的网络编程实战中,把Socket比作打电话,可以更直观的理解这个过程:
你可以把整个 TCP 的网络交互和数据传输想象成打电话,顺着这个思路想象,socket 就好像是我们手里的电话机,connect 就好比拿着电话机拨号,而服务器端的 bind 就好比是去电信公司开户,将电话号码和我们家里的电话机绑定,这样别人就可以用这个号码找到你,listen 就好似人们在家里听到了响铃,accept 就好比是被叫的一方拿起电话开始应答。至此,三次握手就完成了,连接建立完毕。
接下来,拨打电话的人开始说话:“你好。”这时就进入了 write,接收电话的人听到的过程可以想象成 read(听到并读出数据),并且开始应答,双方就进入了 read/write 的数据传输过程。
最后,拨打电话的人完成了此次交流,挂上电话,对应的操作可以理解为 close,接听电话的人知道对方已挂机,也挂上电话,也是一次 close。
在整个电话交流过程中,电话是我们可以和外面通信的设备,对应到网络编程的世界里,socket 也是我们可以和外界进行网络通信的途径。
Socket套接字支持网络上两台以上的设备进行通信,因为Socket有双个缓冲区,进行的是双全工通信。
- 服务器通过accept函数返回值可以获得客服端的套接字,我们就可以对客服端进行IO(读写)操作
- 客服端通过connect函数的第一个参数绑定服务器,我们就可以对服务器进行IO(读写)操作
- 将套接字看做文件描述符使用,更好理解
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:用于服务器和客户端,创建套接字,返回一个可操作的文件描述符
参数使用:
int socket(int domain,int type,int protocal);
参数一:表示ip地址类型,常用的有两种
其中AF_INET表示IPv4地址,比如127.0.0.1,这是一个本机测试ip
其中AF_INET6表示IPv6地址,比如2001:3CA1:10F:1A:121B:0:0:10参数二:表示数据传输方式/套接字类型,常见两种
SOCK_STREAM(流格式套接字/面向连接的套接字)
SOCK_DGRAM (数据报套接字/无连接的套接字)- 参数三:表示传输协议,理论上前两个参数已经可以推演出采用哪种协议主要是为了解决,两种不同的协议支持同一种地址类型和数据型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。如果两种情况只有一个协议满足条件,可以将protocol 的值设为 0,系统自动推演出采用哪种协议
- 返回值:返回一个套接字(文件描述符fd)
作用:用于服务器,给sockfd套接字绑上本机地址和使用端口,确定了服务器的身份
参数使用:
Int bind(int sockfd,const struct sockaddr*addr,socklen_t addrlen);
- 参数一:套接字的fd(文件描述符),socket()函数的返回值
- 参数二:结构体 ip+port(端口)
struct sockaddr_in{ (涉及强制转换sockaddr_in ->sockaddr 参考)
short int sin_family; //地址族
unsigned short int sin_port; //端口号
struct in_addr sin_addr; //IP地址
}
struct in_addr {
__be32 s_addr;
};
- 参数三:结构体的字节长度
- 返回值:判断成功失败
函数作用:用于服务器,使socket处于监听模式,监听时候有客户端连接,并放入队列(也可以说,设置同时与服务器建立连接的上限)(同时进行3次握手连接的客户端数量)
参数使用:
int listen(int sockfd,int backlog);
- 参数一:bind绑定ip和端口的套接字
- 参数二:请求链接客户端队列的最大存放数目
- 返回值:判断成功失败
函数作用:用于服务器,接收一个客户端的连接请求,并返回连接客户端的套接字便于IO操作,如果没有客户连接会阻塞等待。
参数使用:
int accept(int sockfd,struct sockaddr*addr,socklen_t*addrlen)
- 参数一:服务器的套接字(也叫监听套接字),表明了自己的身份
- 参数二:传出参数,跟我建立连接的客户端的结构体(内含客户端ip+端口)
- 参数三: 结构体长度的指针 &sizeof()
- 返回值:连接客户端的套接字
函数作用:用于客户端,函数可以和自动与远端服务器建立连接
参数使用:
int connect(int sockfd,struct sockaddr*serv_addr,int addrlen)
- 参数一:传入参数,文件描述符绑定连接成功的服务器套接字便于在客户端对服务器进行IO操作
- 参数二:绑定我要链接服务器的结构体(需要初始化绑上ip和断口),表明目的
- 参数三:结构体的长度
服务端
#include
#include
#include
#include
#include
#include
#include
#include
#define SER_PORT 8000
int main(void)
{
int sockfd,connfd;//
int len;
char wbuf[1024];
char rbuf[1024];
struct sockaddr_in serveraddr,clientaddr; //两个结构体 一个用于绑定身份到套接字 一个用于接收客服端的结构体
//1.创建监听套接字
sockfd = socket(AF_INET,SOCK_STREAM,0);
//2.bind(通信需要套接字 把家的地址 门牌号绑上去 ip和端口)
bzero(&serveraddr,sizeof(serveraddr)); //类似memset 清空结构体
//地址族协议,选择IPV4
serveraddr.sin_family = AF_INET; //属于ipv4还是ipv6
//IP地址 本机任意可用ip地址
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(SER_PORT);//端口号
bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
//3.监听 和服务器连接的总和
listen(sockfd,128);
int size = sizeof(clientaddr);
//4.accept 阻塞监听 客服端链接的请求
connfd = accept(sockfd,(struct sockaddr *)&clientaddr,&size);
//输出客服端的ip和端口
char ipstr[128];
printf("client ip%s ,port %d\n",inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,ipstr,sizeof(ipstr)),
ntohs(clientaddr.sin_port));
//5.处理客户端请求
//读和写
while(1)
{
memset(wbuf,0,sizeof(wbuf));//清空
memset(rbuf,0,sizeof(wbuf));
//接收消息
int len = read(connfd,rbuf,sizeof(rbuf));
if(len==0)//表示断开连接
{
printf("client is close....\n");
}
printf("receive from client:%s",rbuf);
//发送消息
printf("send to client:");
fgets(wbuf,sizeof(wbuf),stdin);
write(connfd,wbuf,strlen(wbuf));
}
close(connfd);
close(sockfd);
return 0;
}
客户端
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
#define SER_PORT 8000
int main(void)
{
int sockfd;
struct sockaddr_in serveraddr;
int len;
char wbuf[1024],rbuf[1024];
//1、socket 通信用套接字,创建一个sockfd
sockfd = socket(AF_INET,SOCK_STREAM,0);
char ipstr[]="127.0.0.1";
//2、编辑要连接的服务器地址,并绑定
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET; //设置地址族协议
serveraddr.sin_port = htons(SER_PORT); //设置端口号
inet_pton(AF_INET,ipstr,&serveraddr.sin_addr.s_addr);//设置ip地址 点分十进制转成网络字节序
//2、connect 连接服务器 sockfd传出服务器套接字
connect(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
//3、读写
while(1)
{
memset(wbuf,0,sizeof(wbuf));
memset(rbuf,0,sizeof(rbuf));
//发送消息
printf("send to server:");
fgets(wbuf,sizeof(wbuf),stdin);
write(sockfd,wbuf,strlen(wbuf));
//接收消息
len=read(sockfd,rbuf,sizeof(rbuf));
if(len==0)//表示断开连接
{
printf("server is close....\n");
}
printf("receive from server:%s",rbuf);
}
//4、close
close(sockfd);
return 0;
}
简单的说就是即从客户端收到什么数据,就发送什么数据回去
前提须知:read读取不到信息会阻塞等待!
执行过程: 1 ->2->5->6->3->4
/* 客服端部分 */
1 scanf("%s",wbuf);//等待键盘输入
2 write(sfd,wbuf,strlen(wbuf));//写入服务器
3 read(sfd,rbuf,sizeof(rbuf));//等待客服端写会
4 printf("%s\n",rbuf);//打印内容
----------------------分割线------------------------------
/*服务器部分*/
5 read(confd,buf,sizeof(buf)); //阻塞等待
6 write(confd,buf,len); //写会客服端
流程
- 首先客服端与服务器建立连接
- 客服端等待键盘输入(scanf 或者 fget)
- 如果此时键盘输入回车确认 客服端write写入服务器 服务器read读到了信息
- 服务器再将read读到的信息write写回客服端 客服端read接受到信息并打印
服务端:
#include
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int sockfd,confd;
char ipstr[128];
int size;
char buf[1024];
int i;
int len;
//两个结构体 一个用于绑定身份到套接字 一个用于接收客服端的结构体
struct sockaddr_in serveraddr,clientaddr;
//1.创建监听套接字
sockfd = socket(AF_INET,SOCK_STREAM,0);
//2.bind(通信需要套接字 我把我家的地址 门牌号绑上去 ip和端口)
bzero(&serveraddr,sizeof(serveraddr)); //类似memset 清空结构体
//地址族协议,选择IPV4
serveraddr.sin_family = AF_INET; //属于ipv4还是ipv6
//IP地址 本机任意可用ip地址
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(8002);//端口号
bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
//3.监听 128为服务器连接的总和
listen(sockfd,128);
size = sizeof(clientaddr);
//4.accept 阻塞监听 客服端链接的请求
//参数二结构体的转换
confd = accept(sockfd,(struct sockaddr *)&clientaddr,&size);
//输出客服端的ip和端口
printf("client ip%s ,port %d\n",inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,ipstr,sizeof(ipstr)),
ntohs(clientaddr.sin_port));
//5.处理客户端请求
//读和谐
while(1)
{
len=read(confd,buf,sizeof(buf)); //接受不到 阻塞等待
#if 0 //if 0 endif 之间的大小写转换代码已经屏蔽
i=0;
while(i
客户端:
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc,char* argv[])
{
int sfd;
struct sockaddr_in sfdaddr; //指定要连接服务器的结构体 ip 端口
int len;
//char buf[1024];
char wbuf[1024];
char rbuf[1024];
//1.socket 通信用套接字,创建一个socket
sfd = socket(AF_INET,SOCK_STREAM,0);
char ipstr[] = "127.0.0.1"; //或者本机测试ip
//char ipstr[] = "192.168.3.106"; //要连上的ip地址
//初始化地址
bzero(&sfdaddr,sizeof(sfdaddr));
sfdaddr.sin_family = AF_INET;
sfdaddr.sin_port = htons(8002);
inet_pton(AF_INET,ipstr,&sfdaddr.sin_addr.s_addr); //转换ip 保存到结构体内
//2.connect 主动连接服务器 sfd返回客服端的套接字(文件描述符)
connect(sfd,(struct sockaddr *)&sfdaddr,sizeof(sfdaddr));
//3.读写
while(1)
{
memset(wbuf,0,1024);
memset(rbuf,0,1024);
scanf("%s",wbuf);
write(sfd,wbuf,strlen(wbuf));
len=read(sfd,rbuf,sizeof(rbuf));
//write(STDOUT_FILENO,buf,len);
printf("%s\n",rbuf);
}
//4.close
close(sfd);
return 0;
}