socket详解
什么是Socket?
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件。
常见的模式有:c/s模式和PtP模式。
套接字的功能:
不仅 可以本地单机进行,也可跨网络进行通信。
基本数据结构:
Socket中最常用的数据结构,sockaddr_in和sockaddr。原来它两包含的数据都是一样,但他们在使用上有区别:sockaddr是给操作系统用的,程序员用sockaddr_in来区分地址和端口。
如图所示:
常见的函数:
#include <sys/types.h>
#include <sys/socket.h>
socket()函数:
int socket( int family,int type,int protocol)
返回值:成功返回非负描述符,出错返回-1。
参数:
family
family | 描述 |
---|---|
AF_INET |
基于IPv4 Internet协议。TCP和UDP是该协议族的常见协议。 |
AF_INET6 |
IPv6基于Internet的协议。TCP和UDP是该协议族的常见协议。 |
AF_UNIX |
本地通讯协议族 高效率和低开销使它成为一种很好的IPC(进程间通信)形式。 |
type 选择套接字使用的通信类型
类型 | 描述 |
---|---|
SOCK_STREAM |
提供顺序,可靠,全双工,基于连接的字节流。可以支持带外数据传输机制。TCP协议基于此套接字类型。 |
SOCK_DGRAM |
支持数据报(无连接,固定最大长度不可靠的消息)。UDP协议基于此套接字类型。 |
SOCK_SEQPACKET |
为固定最大长度的数据报提供顺序可靠的基于双向连接的数据传输路径; 每个读取呼叫都需要消费者读取整个数据包。 |
SOCK_RAW |
提供原始网络协议访问。这种特殊类型的套接字可以用来手动构造任何类型的协议。此套接字类型的常见用途是执行ICMP请求(如ping)。 |
SOCK_RDM |
提供不保证订购的可靠数据报层。这很可能在您的操作系统上未实现。 |
类型 解释
原始套接字SOCK_RAM和(标准套接字)SOCK_STREAM、SOCK_DGRAM的区别:
原始套接字可以自行组装数据包(伪装本地 IP,本地 MAC,TCP/UDP),可以接收本机网卡上所有的数据帧(数据包),而流式套接字只能 读取TCP协议的数据,数据包套接字只能读取UDP协议的数据。因此要访问其他协议发送数据必须使用原始套接字
如图所示:
protocol:
当protocol为0时,会自动选择type类型对应的默认协议,如果是原始套接字,这儿不能写0。另写一篇讲解原始套接字。
bind()函数
int bind(int sockfd, struct sockaddr *addr, int addrlen);
bind()函数把一个地址族中的特定地址赋给socket。
参数:
sockfd:socket文件描述符。
addr:struct sockaddr *addr的指针,绑定给sockfd的协议地址。
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[ 8]; /* Same size as
};
addrlen:对应一个地址的长度
绑定地址时,还涉及到一个字节序问题:网络字节序和主机字节序
主机字节序:
不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机序。
Intelx86 小端字节序
Motorola680x、HP、PowerPC 大端字节序
网络字节序:
使用统一的字节顺序,避免兼容性问题
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
字节顺序转换
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
eg:
struct sockaddr_in serverAddr;
bzero(&serverAddr,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(5050);
serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(serverSocket,(struct sockaddr *)& serverAddr,szieof(serverAddr))<0)
{
printf(“绑定失败”);
return -1;
}
listen()、connect()函数
如果作为一个服务器,在调用socket()函数、bind()函数之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器就会接受到这个请求。
int listen(int sockfd, int queue_length);
int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
listen()函数作用是侦听,是把服务套接字在指定的端口侦听,以被动的方式等待客户端的连接。
queue_length:接受队列的长度,也就是Server程序调用accept函数之前最大允许的连接请求数,多余的会被拒绝。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
accept()函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
int accept(int sockfd,struct sockaddr *addr,int *addrlen);
返回值:产生一个新的socket描述符来描述该连接,这个连接用来与特定的client交换信息。
网络I\O进行读写操作
- read()/write()
- recv()/send()
- recvfrom()/sendto()
recvfrom()/sendto() 与其他有区别:
int recv(int sockfd, void *buf, int len, int flags);
int send(int sockfd, const void *msg, int len, int flags);
ssize_t sendto(int sock, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
sock:一个标识套接口的描述字。
buf:包含待发送数据的缓冲区。
len:buf缓冲区中数据的长度。
flags:调用方式标志位。
to:(可选)指针,指向目的套接口的地址。
tolen:to所指地址的长度。
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr *from,socklen_t *addrlen)
sock:标识一个已连接套接口的描述字。
buf:接收数据缓冲区。
len:缓冲区长度。
flags:调用操作方式。
from:(可选)指针,指向装有源地址的缓冲区。
addrlen:(可选)指针,指向from缓冲区长度值。
close()函数
服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字。
UDP除了可以单播,它还可以广播和组播
int opt=-1;
setsockopt(sock,SOL_SOCKET,SO_BROADCAST,(char*)&opt,sizeof(opt));//设置套接字类型
广播地址:255.255.255.255
demo:
#include"lib.h"
int main()
{
struct sockaddr_in destAddr;
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
memset(&destAddr,0,sizeof(destAddr));
destAddr.sin_family=AF_INET;
destAddr.sin_port=htons(6666);
destAddr.sin_addr.s_addr=inet_addr("255.255.255.255");
int opt=-1;
if(setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&opt,sizeof(opt))==-1)
{
perror("setsockopt");
return -1;
}
while(1)
{
char buf[1024]={0};
scanf("%s",buf);
if(sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&destAddr,sizeof(destAddr))==-1)
{
perror("sendto");
return -1;
}
}
}
/*接收套接字加入多播组*/
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("225.0.0.6");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq))
组播地址:224.0.1.0~238.255.255.255
demo:
#include"lib.h"
int main()
{
char buf[1024]={0};
struct sockaddr_in destAddr;
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
memset(&destAddr,0,sizeof(destAddr));
destAddr.sin_family=AF_INET;
destAddr.sin_port=htons(8899);
destAddr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(sockfd,(struct sockaddr *)&destAddr,sizeof(destAddr))==-1)
{
perror("bind");
return -1;
}
//设置组播
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("225.0.0.6");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
if(setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq))==-1)
{
perror("setsockopt");
return -1;
}
while(1)
{
memset(buf,0,sizeof(buf));
recvfrom(sockfd,buf,sizeof(buf),0,NULL,NULL);
printf("buf=%s\n",buf);
}
return 0;
}