网络通信实质上也是实现进程间通信,只是与之前进程间通信不同的是,现在在不同的计算机上进行进程间通信。比如:利用QQ工具实现聊天,在两个电脑上有不同的QQ进程之间在通信。而网络通信是如何使用进程间通信呢?采用的是socket技术。下面将对socket技术进行学习;
一、socket介绍
所谓 socket(套接字),本身的意思是插座的意思。对网络中不同主机上的应用进程之间进行双向通信的端点的抽象 。socket将数据包的层层协议进行简化了,将四层网络模型简化成端到端的通信。一个sicket就是网络上进程通信的一端。它能够将层层协议的数据封装好,我们只需要学习使用对应的API即可。
socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的 API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket 通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段信息。socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。
LINUX中一切皆文件!socket质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
socket通信分两部分:
- 服务器端:被动接受连接,一般不会主动发起连接
- 客户端:主动向服务器发起连接
socket是一套通信的接口,Linux 和 Windows 都有,但是有一些细微的差别.
socket是通过文件描述符操作数据,先通过fd往主机A中的写缓冲区写入数据,数据经过四层网络模型加上对应的封装,再通过数据链路层传输至主机B,主机B经过分用,将数据传至读缓冲区,主机B的fd通过读缓冲区获得主机A传递的数据。我们只需要调用API进行数据的读与写,不需要去查看底层是如何实现的。
二、字节序
2.1简介
现代 CPU 的累加器一次都能装载(至少)4 字节(这里考虑 32 位机),即一个整数。那么这 4字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题。
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。
字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。大端字节序是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。
2.2举例
小端字节序:
0X 01 02 03 04 四个字节;
内存方向:低位——高位;
内存存储顺序:04 03 02 01(高字节在高位,低字节在低位);
大端字节序:
0X 01 02 03 04 四个字节 ;
内存方向:低位——高位;
内存存储顺序: 01 02 03 04(高字节在低位,低字节在高位);
不同计算机的存储方向不同,那如果不同大小端的主机进行通信时, 如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。
2.3判断大小端
#include <stdio.h>
int main()
{
union
{
short value;//两个字节;
char bytes[sizeof(short)];//char[2];
}text;
text.value=0x0102;
if(text.bytes[0]==1&&(text.bytes[1]==2))
{
printf("该系统为大端存储\n");
}
else if(text.bytes[0]==2&&(text.bytes[1]==1))
{
printf("该系统为小端存储\n");
}
else
{
printf("未知\n");
}
return 0;
}
运行结果:
程序中用到联合体,联合体又称共用体,是指在同一段内存单元中存放不同类型的变量,先将数据通过value存储,再通过数组取出来,使之上是同一块地址的数据。
三、字节序转换函数
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的接收。
解决问题的方法是:发送端总是要把数据换成大端字节数据后在发送,而接收端知道对方传过来的数据总是以大端字节序,所以接收端可以根据自身采用的字节决定是否对接收到的数据进行转化(小端机转化,大端机不转化)。
网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。
#include <arpa/inet.h>
// 转换端口
uint16_t htons(uint16_t hostshort);
// 主机字节序 - 网络字节序(short类型,端口16位)uint16_t ntohs(uint16_t netshort);
//网络字节序 - 主机字节序
// 转IP
uint32_t htonl(uint32_t hostlong);
// 主机字节序 - 网络字节序(long类型,IP32位)
uint32_t ntohl(uint32_t netlong);
// 主机字节序 - 网络字节序
案例操作:
#include <stdio.h>
#include <arpa/inet.h>
int main()
{
//htons: 转换端口;
unsigned short a=0x0102;//两个字节,在计算机中存储,小端存储;
printf("%x\n",a);//输出结果应该为01 02
unsigned short b= htons(a);//转换为网络字节序,大端排序。
printf("%x\n",b);//输出结果应该为02 01
printf("-------------------------\n");
//htonl:转化IP
char buf[4]={192,168,1,100};//四个字节,在计算机中存储,小端存储;
int n=* (int *)buf;
int sum= htonl(n);
unsigned char *p=(char *)∑
printf("%d %d %d %d\n",*p,*(p+1),*(p+2),*(p+3));//输出结果应该为100 1 168 192
printf("-------------------------\n");
//ntohl:转IP
unsigned char buf1[4]={1,1,168,192};//四个字节,网络字节序,大端排序;
int n1=*(int*)buf1;
int sum1=ntohl(n1);
unsigned char *p1=(unsigned char *)&sum1;
printf("%d %d %d %d\n",*p1,*(p1+1),*(p1+2),*(p1+3));//输出结果应该为 192 168 1 1
printf("-------------------------\n");
//ntohs:转换端口;
unsigned short a1=0x3412;//两个字节,在网络字节序,大端排序;
printf("%x\n",a1);//输出结果应该为34 12
unsigned short b1=ntohs(a1);//转化为计算机存储,小端存储;
printf("%x\n",b1);//输出结果应该为12 34
return 0;
}
运行结果:
四、socket地址
客户端访问——>服务器;服务器需要告诉客户端IP、端口号;
socket是一个结构体,用来封装端口号和IP等信息。
后面socket相关的api中需要用到socket地址;
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr;
#include <bits/socket.h>
typedef unsigned short int sa_family_t;
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称 domain)和对应的地址族入下所示:
sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:
由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
上面的结构体是给IPV4用的,后面的这个才是为IPV6应用;
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;
__ss_padding于存放 socket 地址值,包括IPV6的端口号,流标识,地址,范围ID等;
专用 socket 地址
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
#include <netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family;
/* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in6
{
sa_family_t sin6_family;
in_port_t sin6_port;
/* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
五、IP地址转换
通常,人们习惯可读性好的字符串表示IP地址,比如用点分十进制字符串表示IPV4地址,以及用十六进制数字串表示IPV6地址。但是编程的时候,我们需要先把它们转化为整数(二进制数)方便实用,而记录日志时则相反,我们要把整数表示的IP地址转化为可读的字符串。下面3个函数可用于点分十进制字符串表示的IPV4地址和网络字节序整数表示的IPV4地址之间的转换。
1、将字符串的IP转化成整数;
2、主机、网络字节序的转换。
下面的只适合IPV4的函数操作:
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:
#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
#include <arpa/inet.h>
#include <stdio.h>
int main()
{
//将点分十进制的IP字符串转化为网络字节序的整数;
//创建一个IP字符串;
char buff[]="192.168.1.4";//点分十进制字符串;
unsigned int num=0;
inet_pton(AF_INET,buff,&num);
unsigned char *p=(unsigned char *)(&num);
printf("%d %d %d %d\n",*p,*(p+1),*(p+2),*(p+3));
printf("------------------------\n");
//将网络字节的IP整数转化为十进制的字符串;
char buff1[16];
inet_ntop(AF_INET, &num, buff1, 16);
printf("%s\n",buff1);
return 0;
}
六、TCP通信流程
TCP与UDP:
都是传输层协议;
UDP:用户数据报协议,面向无连接,可以单播,多播,广播;面向数据报,不可靠。
TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输。
UDP | TCP | |
是否创建连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠 | 可靠的 |
连接的对象个数 | 一对一、一对多、多对一、多对多 | 支持一对一 |
传输方式 | 面向数据报 | 面向字节流 |
首部开销 | 8字节 | 最少20字节 |
适应场景 | 实时应用 | 可靠性高的应用 |
TCP通信流程:
服务器端(被动接收连接的角色)
1、创建一个 用于监听的套接字(fd);
监听:监听有客户端的连接;
套接字:实质就是一个文件描述符;
2、将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息);
客户端连接服务器的时候使用的就是这个IP和端口。
3、设置监听,监听的fd开始工作。
4、阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)。
5、通信
-接收数据
-发送数据
6、通信结束,断开连接。
客户端
1、创建一个用于通信的套接字(fd)
2、连接服务器,需要指定连接的服务器的IP和端口;
3、连接成功了,客户端直接与服务器通信。
-接收数据
-发送数据
4、通信结束,断开连接。
七、socket函数
涉及头文件:
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略;
int sockrt(int domain, int type,int protocol);
功能:
创建一个套接字;
参数:
domain:协议族;
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)type:通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议protocol:具体的一个协议。一般写0;
如果参数2写的是SOCK_STREAM:流式协议默认使用TCP;
如果参数2写的是SOCK_DGRAM:报式协议默认是UDP;
返回值:
成功,返回文件描述符,操作的就是内核缓冲区;
失败:-1;
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:
绑定,将fd和本地的IP+端口号进行绑定;
参数:
sockfd:通过socket函数获得的文件描述符;
addr:需要绑定的sockrt地址,这个地址封装了ip和端口号的信息;
addrlen:第二个参数结构体占的内存大小;
返回值:
成功:0;
失败:-1;
int listen(int sockfd, int backlog);
功能:
监听这个 socket上的连接;
参数:
sockfd:通过socket函数获得的文件描述符;
backlog:未连接和已经连接的和最大值;5;
返回值:
成功:0;
失败:-1;
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接。
参数:
sockfd : 用于监听的文件描述符
addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
addrlen : 指定第二个参数的对应的内存大小
返回值:
成功 :用于通信的文件描述符
失败 : 1 。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能: 客户端连接服务器
参数:
sockfd : 用于通信的文件描述符
addr : 客户端要连接的服务器的地址信息
addrlen : 第二个参数的内存大小
返回值:成功: 0
失败 :-1
ssize_t write(int fd, const void *buf, size_t count);// 写数据
ssize_t read(int fd, void *buf, size_t count);// 读数据
八、TCP通信实现
8.1服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
// 1、创建用于监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM,0 );
if(lfd==-1)
{
perror("socket");
exit(-1);
}
// 2、绑定
struct sockaddr_in saddr;
saddr.sin_family=AF_INET;
//inet_pton(AF_INET,"192.168.193.128", saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr=INADDR_ANY;
saddr.sin_port=htons(9999);
int ret=bind(lfd,(const struct sockaddr *)&saddr,sizeof(saddr));
if(ret==-1)
{
perror("socket");
exit(-1);
}
//3、监听
ret = listen(lfd,8);
if(ret==-1)
{
perror("listen");
exit(-1);
}
//4、接收客户端连接
struct sockaddr_in clientadder;
socklen_t len=sizeof(clientadder);
int cfd=accept(lfd,(struct sockaddr *)&clientadder, &len);
if(cfd==-1)
{
perror("accept");
exit(-1);
}
//输出客户端信息
char clinetIP[16];
inet_ntop(AF_INET, &clientadder.sin_addr.s_addr,clinetIP,sizeof(clinetIP));
unsigned short clientport=ntohs(clientadder.sin_port);
printf("IP:%s\n",clinetIP);
printf("Port:%d\n",clientport);
//5、通信
//获取客户端数据
char recivebuff[1024]={0};
int num =read(cfd, recivebuff, sizeof(recivebuff));// 写数据
if(num==-1)
{
perror("read");
exit(-1);
}else if(num>0)
{
printf("recive client data:%s\n",recivebuff);
}
else if(num==0)
{
//表示客户端断开连接。
printf("clinet closed...\n");
}
//给客户端发送信息
char *data="hello,1234!";
write(cfd, data, strlen(data));// 读数据
//关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
运行结果:
8.2客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//1、创建套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd==-1)
{
perror("socket");
exit(-1);
}
//2、连接服务器端
struct sockaddr_in serve_addr;
serve_addr.sin_family=AF_INET;
inet_pton(AF_INET,"192.168.254.170",&serve_addr.sin_addr.s_addr);
serve_addr.sin_port=htons(9999);
int ret= connect(fd,(const struct sockaddr *)&serve_addr,sizeof(serve_addr));
if(ret==-1)
{
perror("connect");
exit(-1);
}
//3、进行通信
char * data="I am client\n";
write(fd, data, strlen(data));
char readbuff[1024]={0};
int len =read(fd,readbuff,sizeof(readbuff));
if(len==-1)
{
perror("read");
exit(-1);
}else if(len>0)
{
printf("recive client data:%s\n",readbuff);
}
else if(len==0)
{
//表示服务器端断开连接。
printf("serve closed...\n");
}
//关闭文件描述符;
close(fd);
return 0;
}
运行结果:
上面的代码是,服务器与客户端只能一次通信,各自发送提前准备好的数据;
下面的代码是多次通信,而且客户端的数据由键盘输入,服务器收到什么数据,就发送什么数据。
服务器:
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
// 1、创建用于监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM,0 );
if(lfd==-1)
{
perror("socket");
exit(-1);
}
// 2、绑定
struct sockaddr_in saddr;
saddr.sin_family=AF_INET;
//inet_pton(AF_INET,"192.168.193.128", saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr=INADDR_ANY;
saddr.sin_port=htons(9999);
int ret=bind(lfd,(const struct sockaddr *)&saddr,sizeof(saddr));
if(ret==-1)
{
perror("socket");
exit(-1);
}
//3、监听
ret = listen(lfd,8);
if(ret==-1)
{
perror("listen");
exit(-1);
}
//4、接收客户端连接
struct sockaddr_in clientadder;
socklen_t len=sizeof(clientadder);
int cfd=accept(lfd,(struct sockaddr *)&clientadder, &len);
if(cfd==-1)
{
perror("accept");
exit(-1);
}
//输出客户端信息
char clinetIP[16];
inet_ntop(AF_INET, &clientadder.sin_addr.s_addr,clinetIP,sizeof(clinetIP));
unsigned short clientport=ntohs(clientadder.sin_port);
printf("IP:%s\n",clinetIP);
printf("Port:%d\n",clientport);
//5、通信
//获取客户端数据
char recivebuff[1024]={0};
while(1){
int num =read(cfd, recivebuff, sizeof(recivebuff));// 读数据
if(num==-1)
{
perror("read");
exit(-1);
}else if(num>0)
{
printf("recive client data:%s\n",recivebuff);
}
else if(num==0)
{
//表示客户端断开连接。
printf("clinet closed...\n");
break;
}
//给客户端发送信息
write(cfd, recivebuff, sizeof(recivebuff));// 写数据
// char *data="hello,1234!";
// write(cfd, data, strlen(data));// 读数据
}
//关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
客户端 :
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//1、创建套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd==-1)
{
perror("socket");
exit(-1);
}
//2、连接服务器端
struct sockaddr_in serve_addr;
serve_addr.sin_family=AF_INET;
inet_pton(AF_INET,"192.168.254.170",&serve_addr.sin_addr.s_addr);
serve_addr.sin_port=htons(9999);
int ret= connect(fd,(const struct sockaddr *)&serve_addr,sizeof(serve_addr));
if(ret==-1)
{
perror("connect");
exit(-1);
}
//3、进行通信
char readbuff[1024]={0};
char writebuff[1024]={0};
while(1)
{
memset(writebuff,0,sizeof(writebuff));
printf("请输入内容:\n");
scanf("%s",writebuff);
//char * data="I am client";
write(fd, writebuff, sizeof(writebuff));
sleep(1);
int len =read(fd,readbuff,sizeof(readbuff));
if(len==-1)
{
perror("read");
exit(-1);
}else if(len>0)
{
printf("recive client data:%s\n",readbuff);
}
else if(len==0)
{
//表示服务器端断开连接。
printf("serve closed...\n");
break;
}
}
//关闭文件描述符;
close(fd);
return 0;
}
运行结果:
客户端:
服务器 :
以上是利用socket实现了TCP通信的建立,并实现了客户端与服务器之间进行的数据的收发。
创作不易,感谢大家多多支持!!!