一:什么是socket
刚接触socket的同学想必也知道socket的中文名,套接字,与其说是中文名倒不如说这是什么玩意,我们先不要管中文名的实际意义,我们先来了解一下什么是socket。
我们上网产生的数据都是经过协议栈一层一层的封装然后经网卡发送到网络,经网络发送到服务端,然后服务端又是一层一层的解封装拿到自己想要的数据。
对于协议栈都是集成在操作系统里,我们并不需要关心TCP,UDP等这些协议是如何实现的,我们关心的是我们的应用程序的数据能不能正常的发送出去和接收服务端发回来的数据。这就需要一个桥梁,一端连接操作系统的协议栈,一端连接用户的应用数据。socket就是这个桥梁。
那我们再来理解一下中文名套接字,看了一圈我最赞同的解释是:套接指的是套接管,就是将两根水管套接起来的管子,然后“字”是此连接的数据标识,即一个WORD,所以套接字就是一个标识连接的数据体。
那有的同学有疑问WORD是啥,在linux等系统中“套接字”对应“socket word”,所以“字”也就是对应“word”,这个“word”可能指储存socket的数据标识,因为端口号是两字节,就是一个WORD。
下边的图就很具体,没有上面那么抽象
对于套接字的解释就到这了,实在编不下去了
二:socket通信流程
如TCP的连接流程一样,TCP建链需要三次握手,TCP拆链需要四次挥手,socket通信也有自己的一套流程。
对于客户端:
1,创建一个用于通信的套接字(fd)
2,连接服务器,需要指定连接的服务器的IP 和 端口
3,建立连接成功,客户端和服务器建立连接通道
1>可以发送数据
2>可以接收数据
4,通信结束,断开连接
对于服务端:
1,创建一个用于监听的套接字
1>监听:监听有客户端的连接
2>套接字:这个套接字其实就是一个文件描述符
2,将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
1>客户端连接服务器的时候使用的就是这个IP和端口
3,设置监听,监听的fd开始工作
4,阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字 (fd)
5,服务端和客户端通信
1>接收数据
2>发送数据
6,通信结束,断开连接
三:socket通信函数详解
1,socket()函数
int socket(int domain, int type, int protocol); - 功能:创建一个套接字 - 参数: - domain: 协议族 AF_INET : ipv4 AF_INET6 : ipv6 AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信) - type: 通信过程中使用的协议类型 SOCK_STREAM : 流式协议 SOCK_DGRAM : 报式协议 - protocol : 具体的一个协议。一般写0 - SOCK_STREAM : 流式协议默认使用 TCP - SOCK_DGRAM : 报式协议默认使用 UDP - 返回值: - 成功:返回文件描述符,操作的就是内核缓冲区。 - 失败:-1
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
2,bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命 名 - 功能:绑定,将fd 和本地的IP + 端口进行绑定 - 参数: - sockfd : 通过socket函数得到的文件描述符 - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息 - addrlen : 第二个参数结构体占的内存大小
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
3,listen()函数
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn - 功能:监听这个socket上的连接 - 参数: - sockfd : 通过socket()函数得到的文件描述符 - backlog : 未连接的和已经连接的和的最大值, 5
作为服务端需要时刻监听是否有客户端发来的数据,服务端就是调用listen()来监听建立的socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
4,connect()函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);- 功能: 客户端连接服务器 - 参数: - sockfd : 用于通信的文件描述符 - addr : 客户端要连接的服务器的地址信息 - addrlen : 第二个参数的内存大小 - 返回值:成功 0, 失败 -1
客户端通过调用connect函数来建立与TCP服务器的连接
5,accept()函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); - 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 - 参数: - sockfd : 用于监听的文件描述符 - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)- addrlen : 指定第二个参数的对应的内存大小 - 返回值: - 成功 :用于通信的文件描述符 - -1 : 失败
服务器侧在调用socket()、bind()、listen()之后,就会监听指定的socket地址了。客户端在调用socket()、connect()之后就建立了一条连接通道并发向服务端发送一个请求,服务器监听到这个请求之后,就会调用accept()函数取接收请求。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作
6,read()、write()函数
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是 全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。
网络I/O操作不止是read()/write()函数,下面几组也是的
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
其申明如下:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
7,close()函数
int close(int fd);
close一个socket连接后会立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求
四:socket通信实战
客户端:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>#define SERVER_PORT 8888int main()
{//客户端只需要一个套接字文件描述符,用于和服务器通信int serverSocket;//描述服务器的socketstruct sockaddr_in serverAddr;char sendbuf[200]; //存储 发送的信息 char recvbuf[200]; //存储 接收到的信息 int iDataNum;/*********************************************************************//* 1-创建客户端套接字 *//*********************************************************************/if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0){perror("socket");return 1;}serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(SERVER_PORT);//指定服务器端的ip,本地测试:127.0.0.1//inet_addr()函数,将点分十进制IP转换成网络字节序IPserverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");/*********************************************************************//* 2-连接服务端 *//*********************************************************************/ if(connect(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0){perror("connect");return 1;}printf("连接到主机...\n");/*********************************************************************//* 3-发送接收消息 *//*********************************************************************/ while(1){printf("发送消息:");scanf("%s", sendbuf);printf("\n");send(serverSocket, sendbuf, strlen(sendbuf), 0); //向服务端发送消息if(strcmp(sendbuf, "quit") == 0) break;printf("读取消息:");recvbuf[0] = '\0';iDataNum = recv(serverSocket, recvbuf, 200, 0); //接收服务端发来的消息recvbuf[iDataNum] = '\0';printf("%s\n", recvbuf);}close(serverSocket);return 0;
}
服务端:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>#define SERVER_PORT 8888/*
监听后,一直处于accept阻塞状态,
直到有客户端连接,
当客户端如close后,断开与客户端的连接
*/int main()
{//调用socket函数返回的文件描述符int serverSocket;//声明两个套接字sockaddr_in结构体变量,分别表示客户端和服务器struct sockaddr_in server_addr;struct sockaddr_in clientAddr;int addr_len = sizeof(clientAddr);int clientSocket;char buffer[200]; //存储 发送和接收的信息 int iDataNum;/*********************************************************************//* 1-创建服务端套接字 *//*********************************************************************/if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0){perror("socket");return 1;}memset(&server_addr,0, sizeof(server_addr));//初始化服务器端的套接字,并用htons和htonl将端口和地址转成网络字节序server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);//ip可是是本服务器的ip,也可以用宏INADDR_ANY代替,代表0.0.0.0,表明所有地址server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//对于bind,accept之类的函数,里面套接字参数都是需要强制转换成(struct sockaddr *)//bind三个参数:服务器端的套接字的文件描述符/*********************************************************************//* 2-服务端绑定监听的IP和por *//*********************************************************************/ if(bind(serverSocket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){perror("connect");return 1;}/*********************************************************************//* 3-服务端开始监听 *//*********************************************************************/ if(listen(serverSocket, 5) < 0)//开启监听 ,第二个参数是最大监听数{perror("listen");return 1;}/*********************************************************************//* 4-接收发送消息 *//*********************************************************************/ printf("监听端口: %d\n", SERVER_PORT);//调用accept函数后,会进入阻塞状态//accept返回一个套接字的文件描述符,这样服务器端便有两个套接字的文件描述符,//serverSocket和client。//serverSocket仍然继续在监听状态,client则负责接收和发送数据//clientAddr是一个传出参数,accept返回时,传出客户端的地址和端口号//addr_len是一个传入-传出参数,传入的是调用者提供的缓冲区的clientAddr的长度,以避免缓冲区溢出。//传出的是客户端地址结构体的实际长度。//出错返回-1clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, (socklen_t*)&addr_len);if(clientSocket < 0){perror("accept");}printf("等待消息...\n");//inet_ntoa ip地址转换函数,将网络字节序IP转换为点分十进制IP//表达式:char *inet_ntoa (struct in_addr);printf("IP is %s\n", inet_ntoa(clientAddr.sin_addr)); //把来访问的客户端的IP地址打出来printf("Port is %d\n", htons(clientAddr.sin_port)); while(1){buffer[0] = '\0';iDataNum = recv(clientSocket, buffer, 1024, 0);if(iDataNum < 0){continue;}buffer[iDataNum] = '\0';if(strcmp(buffer, "quit") == 0) break;printf("收到消息: %s\n", buffer);printf("发送消息:");scanf("%s", buffer);send(clientSocket, buffer, strlen(buffer), 0); //服务端也向客户端发送消息 if(strcmp(buffer, "quit") == 0) break; //输入quit停止服务端程序 }close(clientSocket);close(serverSocket);return 0;}
在Linux上用gcc编译:
gcc server.c -o server
gcc client.c -o client
先运行服务端:
再运行客户端:
客户端服务端收发消息: