研究生阶段项目开发用到了socket技术,写个博客简单记录一下socket通信相关的基础知识,包含我个人对socket技术的一些理解,个人经验,如有错误烦请大佬们批评指正
什么是socket
中文翻译过来叫“套接字”,可以理解为一个通信端点,我们都知道主机与主机之间通信是通过ip和端口(传输层和网络层),那么两台主机上的应用程序(应用层)如果想相互交流,也需要借助主机间的通信机制,但是应用程序不是主机本身,想要使用这一机制,就需要借助于socket,也就是说,socket是连接应用层和各种网络协议的接口。
socket的通信机制有点类似于我们用手机通信,当两个人需要远程对话的时候,首先要各有一部手机,然后需要插入电话卡(类似于ip地址和端口号),然后一方拨打另一方的手机号码,接通之后,可以相互发送/接收信息。
socket的类型
流格式套接字
流格式套接字(Stream Sockets)也叫面向连接的套接字,代码中使用SOCK_STREAM表示。提到面向连接,一定是基于TCP协议的,SOCK_STREAM是一种可靠的、双向的通信数据流,在底层实现了重传机制,使得数据可以准确无误地到达另一台计算机。
SOCK_STREAM的特征:
- 数据按照顺序传输(早发送的早到)
- 在传输过程中数据不会丢失(TCP协议保证)
- 数据发送和接收是不同步的(传输的数据存入缓冲区)
实际应用:HTTP协议传输数据
数据报格式套接字
数据报格式套接字(Datagram Sockets),也叫无连接的套接字(听名字就知道基于UDP),代码中使用SOCK_DGRAM表示。拥有UDP协议传输的优缺点,数据传送速度快,消耗小,但是数据容易丢失或损坏
SOCK_DGRAM的特征:
- 快速传输(不注重顺序)
- 数据可能会丢失或损坏(只是小概率事件,只是相对TCP来说不稳定)
- 数据的发送和接收是同步的
- 每次传输的数据大小有限制
实际应用: QQ视频、语音
原始套接字
原始套接字(Raw Socket)是一个特殊的额套接字类型,代码中使用SOCK_RAW表示,原始套接字的特殊之处在于:能够实现从应用层到数据链路层的所有数据操作。TCP/UDP类型的套接字只能访问传输层以及传输层以上的数据(因为TCP/UDP是传输层协议,当传到传输层的时候,下层的IP包头/帧头帧尾都已经被丢弃)
关于原始套接字,在此仅仅作一个简单介绍,我了解和使用的不是很多,不敢妄言,感兴趣的同学可以自行百度。
C/S架构下的socket设计
我参与开发的项目主要是C/S架构(网上的大多数socket教程也都采用C/S架构举例),就从这一方向简单介绍一下。
下图是我自己绘制的Stream Sockets在客户端和服务器端上的建立创建、连接、通信和关闭流程图,并包含了通过程中各种状态的变化。
Datagram Sockets的流程与Stream Sockets流程类似,只是其中的TCP协议换成了UDP协议,少了几次握手和挥手的过程。
socket常用数据结构
socket描述符
int类型,用于保存创建好的套接字
sockaddr
struct sockaddr {
unsigned short sa_family; /* 地址家族, AF_xxx */
char sa_data[14]; /*14字节协议地址*/
};
其中sa_family是两字节的地址家族,一般都是“AF_xxx”的形式,用于指定函数返回的地址信息类型
AF_INET:返回IPV4地址信息
AF_INET6:返回IPV6地址信息
AF_UNSPEC:可以返回任何协议族的地址
我们使用的基本都是第一种。
sa_data包含套接字中的目标地址和端口信息
sockaddr_in
struct sockaddr_in {
short int sin_family; /* 通信类型 */
unsigned short int sin_port; /* 端口 */
struct in_addr sin_addr; /* Internet 地址 */
unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/
};
sin_family:等同于sa_family,实际应用中大多选择AF_INET
sin_port:存储端口号
sin_addr:存储IP地址,使用in_addr这个数据就够,并且要使用网络字节序
sin_zero:是为了让sockaddr与sockaddr_in两个结构体保持大小相同而保留的空字节
设计sin_zero,目的就是能让这两个结构体大小相等,进而能够相互转换,实际编程中多数使用第二个结构体来设置和获取地址信息,而作为参数传递时通常转换成sockaddr结构
in_addr
typedef struct in_addr {
union {
struct{
unsigned char s_b1,s_b2, s_b3,s_b4;} S_un_b;
struct{
unsigned short s_w1, s_w2;} S_un_w;
unsigned long S_addr;
} S_un;
} IN_ADDR;
是一个存储ip地址的共用体,有三种表达方式
第一种用四个字节表示IP地址的四个数字。
第二种用两个双字节表示IP地址
第三种用一个长整型来表示IP地址
给in_addr赋值最简单的方式就是inet_addr()函数,可以将一个代表IP地址的字符串转换为in_addr类型,其反函数是inet_ntoa()。
sockaddr_in ina;
ina.sin_addr.s_addr=inet_addr("192.168.0.1");
实际使用时需要对inet_addr()的返回值进行检查,如果为-1则说明函数错误,如果不检查无符号的-1则与广播地址255.255.255.255相同
网络与主机字节序的相互转换
需要先介绍一下网络字节序和主机字节序
网络字节序
是TCP/IP中已经规定好的一种数据表示格式,是一种固定格式,保证数据在不同主机之间传输时能够被正确解释。网络字节序采用大端存储方式(低位字节放在内存高位地址,高位字节放在内存低位地址)
主机字节序
主机字节序是多样性的,其存储方式取决于CPU等
判断主机字节序的方法
bool am_little_endian()
{
unsigned short i=1;
return (int)*((char*)(&i))?true:false; //返回true则为小端存储
}
转换方法
htons()——主机字节序转换为网络字节序(short类型,两个字节)
htonl()——主机字节序转换为网络字节序(long类型,四个字节)
ntohs()——网络字节序转换为主机字节序(short类型)
ntohl()——网络字节序转换为主机字节序(long类型)
为了程序的可移植性,当数据需要被传输到网络上时,一定要判断主机字节序,并确保其和网络字节序相同
如sockaddr_in结构体中的sin_port和sin_addr就都需要确保为网络字节序
socket相关函数
终于写到这里了。。。以下有些函数是客户端独有的,有些函数是服务器端独有的,为了行文方便,我就混着写了,读者可以参考前面的图示来确定这些函数执行的先后顺序
socket()
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domian,int type,int protocol);
一个一个参数来看
domain:需要设置为AF_INET,等同于sa_family和sin_family。
type:告诉内核,我们使用的socket是SOCK_STREAM还是SOCK_DGRAM类型(一般用不到SOCK_RAW类型)。
protocol:指使用的协议,设置为0时,是默认跟随type参数来选择协议,如果type参数值为SOCK_STREAM,protocol默认为IPPROTO_TCP;如果type值为SOCK_DGRAM,protocol默认为IPPROTO_UDP。
这个函数返回一个int类型套接字,我们后面要用这个套接字实现客户端和服务器的连接
bind()
#include <sys/types.h>
#include <sys/socket.h>
int bind(