1、预备知识
1.1、套接字socket
套接字是在进行网络连接时最重要的部分之一!
套接字socket就相当于是,一个文件操作符,不占空间。但是这个套接字对应这两个缓冲区:发送端缓冲区和接收端缓冲区,如下图所示。两个套接字的连接就是通过IP和端口。
1.2、IP和端口
- IP:在网络环境中唯一,表示一台主机
- 端口:在主机中唯一,表示一个进程
在网络通信中,只有找到这二者才可以进行通信连接
1.3、网络字节序列
- 原理:因为每一个机器内部对变量的字节存储顺序不同(有的系统是高位在前,底位在后,而有的系统是底位在前,高位在后 ),而网络传输的数据大家是一定要统一顺序的。所以对于内部字节表示顺序和网络字节顺序不同的机器,就一定要对数据进行转换。下面提供两种方式转换。
1.3.1、IP转换函数(其一,该函数比较常用,在linux帮助手册的第11章):
- 头文件 #include <arpa/inet.h>
-
函数int inet_pton(int af, const char *src, void *dst);
- 参数1:int af写是AF_INET (用于IPv4)、AF_INET6 (用于IPv6)
- 参数2:*src是来源地址
- 参数3:传入一个字符串,接收转换后的数据。
- 返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
- 作用:将“点分十进制” -> “二进制整数”
-
函数const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
-
参数1:int af写是AF_INET (用于IPv4)、AF_INET6 (用于IPv6)
-
参数2:*src是来源地址
-
参数3:传入一个字符串,接收转换后的数据
-
参数4:第三个参数dst的大小(用sizeof)
-
返回值:若成功则为指向结构的指针,若出错则为NULL
-
作用:将“点分十进制” -> 字符串
-
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,
返回值:如果第一个参数af 不包含有效的地址族,则返回负值并将 errno 设置为 EAFNOSUPPORT。 如果 src 不包含表示指定地址族中的有效网络地址的字符串,则返回0。
如果网络地址成功转换,则返回正值。
1.3.2、IP转换函数(其二)
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //主机字节序转网络字节序,对无符号长型进行操作4bytes
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
上面的函数中 h表示host,n表示network,l表示32位长整数,s表示16位短整数。 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
1.4、使用socket连接时的主要流程
TCP协议:为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。
-------------------------前期工作----------------------
服务器工作流程:
- 建立socket对象,指定通信协议(TCP)
- socket对象和ip地址端口(prot)进行bind绑定
- 监听操作(=》读写)
客户端的工作流程:
- 建立socket对象,指定通信协议(TCP)
- 指定ip地址,端口,建立连接connect()
- 客户端发送连接请求
- read等待服务器的应答(接受我连接请求申请)
-------------------------信息交互工作----------------------
双方通信
----------------------------收工工作-------------------------
关闭socket,断开连接
2、socket编程解析
2.1、编写代码的流程
-
创建套接字
-
绑定bind
-
监听listen
-
接受accept
-
和客户端交互数据read write
2.2、创建套接字
-
头文件:#include <sys/types.h> #include <sys/socket.h>
-
函数:int socket(int domain, int type, int protocol);
-
参数1:domain
-
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
-
AF_INET6 与上面类似,不过是来用IPv6的地址
-
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
-
-
参数2:type
-
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类
型,这个socket是使用TCP来进行传输。 -
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
-
SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的 接受才能进行读取。
-
SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
-
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数 据包的顺序
-
-
参数3:protocol: 0 默认协议
-
返回值:成功返回一个新的文件描述符,失败返回-1,设置errno。
-
2.3、绑定bind
在解释绑定之前要先解释一下sockaddr数据结构和sockaddr_in数据结构。
在以前的时候是使用struct sockaddr没有细分,现在的话是使用struct sockaddr_in,而现在sockaddr退化成了(void *)的作用(即:返回出一个无类型指针,传递一个地址给函数),但是在编程中的bind还是保存着以前的struct sockaddr的,所以我们在写bind函数的时候需要把struct sockaddr_in强转成
struct sockaddr。
2.3.1、sockaddr数据结构
struct sockaddr
{
//地址家族通常是AF_xxx的形式。通常大多用的是都是AF_INET,代表TCP/IP协议族
sa_family_t sa_family;
// sa_data是14字节协议地址
char sa_data[14];
};
2.3.2、sockaddr数据结构
struct sockaddr_in结构体
struct sockaddr_in
{
//Address family一般来说AF_INET(地址族)PF_INET(协议族)
short sin_family;
//端口号(必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字)
unsigned short sin_port;
//IP地址 (网络字节顺序)
struct in_addr sin_addr;
//没有实际意义,只是为了 跟SOCKADDR结构在内存中对齐
unsigned char sin_zero[8];
};
2.4、绑定bind
在服务器程序中,监听的网络地址和端一般IP和端口是不会变的,一个客户端要是知道服务器的地址和端口号就可以向服务器发起连接,所以服务器需要调用bind来绑定一个固定的网络地址和端口号。
- 文件:#include <sys/types.h> #include <sys/socket.h>
- 函数:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 参数解析
- 参数1:sockfd: socket文件描述符,(就是前面我们创建的套接字)
- 参数2:addr: 构造出IP地址加端口号
- 参数3:addrlen: sizeof(addr)长度
- 返回值:成功返回0,失败返回-1, 设置errno
其原理就是把传进来的参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。但是在绑定函数执行之前我们需要向先执行一下函数,为绑定函数做准备。
struct sockaddr_in servaddr; //创建一个struct sockaddr_in结构体
bzero(&servaddr,sizeof(servaddr)); //出始化一下这个地址(就相当于malloc出一个地址空间,就要紧接着memset一个道理)
servaddr.sin_family = AF_INET; //地址家族赋值,通常大多用的是都是AF_INET,代表TCP/IP协议族
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY这个宏就是可以将任意本机可用IP地址转成网络字节序,可用ifconfig查询虚拟机的IP
servaddr.sin_port = htons(8000); //短整型无符号端口号转成网络字节序列
//当以上这些函数执行完了之后我们再执行bind函数
bind( sfd, ( struct sockaddr*) &servaddr, sizeof(servaddr));
2.5、监听listen
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的 accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未 accept的客户端就处于连接等待状态。listen()函数声明sockfd处于监听状态,并且最多允许有 backlog个客户端处于连接待状态(在listen()函数中可以设置这个backlog数量的多少),如果接收到更多的连接请求就忽略。(说白了,这函数的功能就是要和客户端建立三次握手和阻碍超过指定数量外的客户端)。
- 头文件:#include <sys/types.h> #include <sys/socket.h>
- 函数:int listen(int sockfd, int backlog);
- 参数解释:
- 参数1:sockfd:socket函数返回的文件操作符
- 参数2:backlog:排队建立3次握手队列和刚刚建立3次握手队列的链接数和(即可以监听客户端的个数)
- 返回值:成功返回 0,失败返回-1。
注释解析三次握手:就相当于是有两个人A和B,A代表客户端,B代表服务器,A见到B后给B一个微笑,这就算是第一次握手,B看到A微笑后,回应微笑和挥一次手,这就算是第二次握手了,A看到B挥手,自己也挥手,这就算是第三次握手了。
2.6、接受accept
该函数在三次握手后执行的函数
- 头文件:#include <sys/types.h> #include <sys/socket.h>
- 函数:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 参数解析
- 参数1:sockfd: socket文件描述符,(就是前面我们创建的套接字)
- 参数2:addr: 传出参数,返回链接客户端地址信息,含IP地址和端口号
- 参数3:addrlen: 传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
- 返回值:成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
执行完这函数,我们能获得到一个全新的套接字,和该套接字的IP端口,这个套接字就是用来专门和客户端通信用的。
然后我们再通过
char ipstr[128];
inet_ntop(AF_INET,&addr.sin_addr.s_addr ,ipstr ,sizeof(ipstr));
ntohs(addr.sin_port); //返回端口号
示例代码
服务器代码
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SERVER_PORT 8000 //这里定义一个端口
int main(void)
{
int sfd,cfd; //创建两个文件操作符,用来存储两个套接字
char ipstr[128]; //用来存储cfd的id
int addlen; //用来计算cfdaddr大小的
char buf[1024],send_buf[1024]; //发送接收的缓存区
struct sockaddr_in sfdaddr,cfdaddr;
/*1、socket监听套接字*/
//创建一个套接字,成功就返回一个文件描述符
sfd = socket(AF_INET,SOCK_STREAM,0);
/*2、bind绑定一个地址*/
//初始化sockaddr_in结构体
bzero(&sfdaddr,sizeof(sfdaddr));
sfdaddr.sin_family = AF_INET;
sfdaddr.sin_port = htons(SERVER_PORT);
sfdaddr.sin_addr.s_addr = htonl(INADDR_ANY); //任意本机可用IP(可以使用ifconfig查看)
bind(sfd,(struct sockaddr*)&sfdaddr,sizeof(sfdaddr));
/*3、监听listen*/
listen(sfd,128);
/*4、accept接受 */
addlen = sizeof(cfdaddr);
//通信套接字 阻塞监听客户端链接请求,如果是放在while里则是可以接受多个客户端
cfd = accept(sfd,(struct sockaddr*)&cfdaddr,&addlen);
printf("client ip :%s,port %d\n",inet_ntop(AF_INET,&cfdaddr.sin_addr.s_addr,ipstr,sizeof(ipstr)),ntohs(cfdaddr.sin_port));
/*和客户端交流数据*/
//可以使用send recv 或者read write
while(1)
{
//接受
printf("客户端发来的消息为:\n");
recv(cfd,buf,sizeof(buf),0);
fputs(buf,stdout);
//发送
printf("給客户端发送的数据:\n");
fgets(send_buf,sizeof(send_buf),stdin);
send(cfd,send_buf,strlen(send_buf),0);
memset(buf,0,sizeof(buf));
memset(send_buf,0,sizeof(send_buf));
}
close(cfd);
close(sfd);
return 0;
}
代码,这里的代码也可以不要,在linux终端输入 nc IP号 端口号就可以连接服务器,比如NC 127.0.01
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SERVER_PORT 8000 //这里定义一个端口
int main(void)
{
int sfd;
char buf[1024],send_buf[1024];
//char ipstr[] = "192.168.158.130"; //如果客户端是windows就要写虚拟机上的IP
//sfdaddr.sin_addr.s_addr = htonl(INADDR_ANY); //这个会自动识别
char ipstr[] = "127.0.0.1"; //都在虚拟机上运行写这个
/*1、socket监听套接字*/
//创建一个套接字,成功就返回一个文件描述符
sfd = socket(AF_INET,SOCK_STREAM,0);
//配置结构体,IP转网络字节序
sfdaddr.sin_family = AF_INET;
sfdaddr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET,ipstr,&sfdaddr.sin_addr.s_addr);
/*2、connect连接*/
connect(sfd,(struct sockaddr*)&sfdaddr,sizeof(sfdaddr));
/*请求服务器处理数据 read write*/
while(1)
{
//发送
printf("給服务端发送的数据:\n");
fgets(send_buf,sizeof(send_buf),stdin);
send(sfd,send_buf,strlen(send_buf),0);
//接受
printf("服务端发来的消息为:\n");
recv(sfd,buf,sizeof(buf),0);
fputs(buf,stdout);
memset(buf,0,sizeof(buf));
memset(send_buf,0,sizeof(send_buf));
}
close(sfd);
return 0;
}