概述
文章目录
- 一 什么是套接字Socket
- 1.Socket简介
- 2.Socket的域(domain)
- 3.Socket主要类型(type)
- 4.Socket基本工作流程
- 二 创建套接字Socket
- 1.socket函数
- 三 绑定套接字Socket与主机网络地址
- 1.bind函数
- 2.struct sockaddr与struct sockaddr_in
- 3.常用填充地址信息的方法
- 4.主机字节序与网络字节序
- 四 UDP通信的实现
- 1.recvfrom函数
- 2.sendto函数
- 3.示例
- 五 TCP通信的实现
- 1.listen函数(server端)
- 2.accept函数(server端)
- 3.connect函数(client端)
- 4.write与read函数
- 5.send与recv函数
- 6.示例
- 六 套接字的缓冲区以及阻塞模式
- 1.缓冲区
- 2.使用write/send发送数据
- 3.使用read/recv读取数据
- 七 总结套接字收发数据的过程
一 什么是套接字Socket
1.Socket简介
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口 。
Socket(套接字)可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的API(应用程序编程接口),也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 Socket中,该 Socket通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 Socket中,使对方能够接收到这段信息。 Socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制 。
2.Socket的域(domain)
域指定套接字通信中使用的网络介质。最常见的套接字域是 AF_INET(IPv4)或者AF_INET6(IPV6),它是指 Internet 网络,许多 Linux 局域网使用的都是该网络,当然,因特网自身用的也是它。
3.Socket主要类型(type)
- 流套接字(SOCK_STREAM)
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议 。 - 数据报套接字(SOCK_DGRAM)
数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理 。 - 原始套接字(SOCK_RAW)
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接 。
4.Socket基本工作流程
要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket 。根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤 :
- 服务器监听
所谓服务器监听,是指服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态 。 - 客户端请求
所谓客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端接字提出连接请求 。 - 连接确认
所谓连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,就会响应客户端套接字的请求,建立一个新的线程,并把服务器端套接字的描述发送给客户端。一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,接收其他客户端套接字的连接请求 。
二 创建套接字Socket
1.socket函数
int socket(int domain, int type, int protocol);
/*
1.函数功能:创建套接字
2.参数:
int domain:套接字的域通常为 AF_INET(IPv4)或者AF_INET6(IPV6)
int type:套接字类型通常为 SOCK_STREAM、SOCK_DGRAM
int protocol:
0 :使用默认协议
IPPROTO_TCP:使用TCP协议
IPPROTO_UDP:使用UDP协议
3.返回值:
成功:返回套接字描述符
失败:-1
*/
三 绑定套接字Socket与主机网络地址
1.bind函数
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
/*
1.函数功能:绑定套接字Socket与主机网络地址信息
2.参数:
int sockfd: 套接字描述符
const struct sockaddr *addr:主机地址信息,下文详解
socklen_t addrlen: 参数2的长度(字节)
3.返回值:
成功:0
失败:-1
*/
2.struct sockaddr与struct sockaddr_in
//以下主要摘自LINUX手册
typedef unsigned short int sa_family_t;
/* Structure describing a generic socket address.翻译:描述通用套接字地址的结构 */
struct sockaddr {
sa_family_t sa_family;//地址族
char sa_data[14];//14字节,包含套接字中的目标地址和端口信息
}
/* Structure describing an Internet socket address.翻译:描述Internet套接字地址的结构 */
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
char sin_zero[8];//占位不使用,用来与struct sockaddr对齐
};
/* Internet address */
struct in_addr {
/*uint32_t*/ in_addr_t s_addr;/* address in network byte order地址的网络字节序 */
};
/*sin_addr is the IP host address.
The s_addr member of struct in_addr contains the host interface address in network byte order.
翻译:sin_addr为主机IP地址。struct in_addr的s_addr成员以网络字节顺序包含主机接口地址*/
- 这两个结构体一样大,都是16个字节,而且都有family属性,二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。不同的是:
sockaddr结构体中sa_data成员融合了端口与地址信息,而sockaddr_in结构体用两个成员sin_port和sin_addr分别表示端口号和地址信息 - sin_port和sin_addr都必须是网络字节序(NBO Network byte order),一般可视化的数字都是主机字节序(HBO Host byte order),下文详解。
- sockaddr是给操作系统用的。程序员应使用sockaddr_in来表示地址,把类型、ip地址、端口填充sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给系统调用函数。sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
示例:
int sockfd;
struct sockaddr_in serverAddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* 填充struct sockaddr_in */
bzero(&serverAddr, sizeof(serverAddr));//初始化为0状态 主要是对成员sin_zero[8]清0
serverAddr.sin_family = AF_INET; //设置地址家族
serverAddr.sin_port = htons(SERV_PORT);//端口号1024-65535
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
/* 强制转换成struct sockaddr */
bind(sockfd, (struct sockaddr *) &serverAddr, sizeof(serverAddr));
3.常用填充地址信息的方法
//填充IP地址
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//0.0.0.0 等号后面可以是htonl(0)或者0
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
inet_aton("127.0.0.1",&serverAddr.sin_addr);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
//填充端口
serverAddr.sin_port = htons(1234);//端口号1024-65535
serverAddr.sin_port = htons(0);//随机端口 等号后面可以 0
相关函数:
1. inet_addr
in_addr_t inet_addr(const char *cp);
/*
功能:点分字符串格式地址转网络格式
参数:IPv4地址字符串例如"127.0.0.1"
返回值:
成功:返回网络字节序的地址用于赋值serverAddr.sin_addr.s_addr
失败:-1
*/
2.inet_ntoa 、inet_aton
char *inet_ntoa (struct in_addr in) //net to ascii
/*
功能:网络字节序地址转点分字符串格式地址
参数:传入通用的网络字节序地址struct in_addr sin_addr
返回值:
成功:返回指针指向IPv4点分字符串格式地址 例如"127.0.0.1"
失败:0
*/
int inet_aton(const char *cp, struct in_addr *inp); //ascii to net
/*
功能:点分字符串格式地址转网络格式地址
参数:
cp:IPv4点分字符串格式地址
inp:网络字节序地址struct in_addr sin_addr
返回值:
成功:非0
失败:0
*/
4.htons、htonl
uint16_t htons(uint16_t hostshort);//h host n net s short
uint32_t htonl(uint32_t hostlong);//h host n net l long
/*
功能:将主机字节序的short/long类型数据转为网络字节序类型数据
参数:
short类型数据/long类型
返回值:
成功:网络字节序类型数据
失败:-1
*/
5.inet_pton、inet_ntop
这两个函数是随IPv6出现的函数,对于IPv4地址和IPv6地址都适用,函数中p和n分别代表表达(presentation)和数值(numeric)。地址的表达格式通常是ASCII字符串,数值格式则是存放到套接字地址结构的二进制值。
int inet_pton(int family, const char *strptr, void *addrptr);
//返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
//返回值:若成功则为指向结构的指针,若出错则为NULL
4.主机字节序与网络字节序
NBO : 网络字节序
HBO : 主机字节序
LE little-endian:小端
BE big-endian:大端
- 网络字节序和主机字节序:
网络数据流的地址规定:先发出的数据是低地址,后发出的数据是高地址。
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,为了不使数据流乱序,接收主机也会把从网络上接收的数据按内存地址从低到高的顺序保存在接收缓冲区中。
TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。
tcp/ip规定它们的网络字节序都是大端字节序。主机字节序可能是大端也可能是小端,与主机的cpu有关,与操作系统无关考虑到与协议的一致以及与同类其它平台产品的互通,在程序中发数据包时,将主机字节序转换为网络字节序,收数据包处将网络字 节序转换为主机字节序。网络程序开发时 或是跨平台开发时 应该注意保证只用一种字节序 不然两方的解释不一样就会产生bug。数据在传输的过程中,一定有一个标准化的过程,也就是说:
从主机a到主机b进行通信:a的主机字节序——网络字节序——b的主机字节序
- 大端字节序和小端字节序:
大端字节序存储时值的高位存储在较小的地址,值的低位存储在较大的地址。
小端字节序存储时值的高位存储在较大的地址,值的低位存储在较小的地址。
以0x12345678为例:
地址:0x1000 0x1001 0x1002 0x1003
小端: 78 56 34 12
大端: 12 34 56 78 - 测试主机是大端还是小端的方法:
int main()
{
union
{
short s;
char c[sizeof(short)];
}un;
un.s = 0x0102;
if(sizeof(short)==2)
{
if(un.c[0] == 1 && un.c[1] == 2)
printf("Big-Endiann");
else if(un.c[0] == 2 && un.c[1] == 1)
printf("Little-Endiann");
else
printf("Unknownn");
}
else
print("sizeof(short)=%dn",sizeof(short));
exit(0);
}
四 UDP通信的实现
在创建并绑定套接字之后,我们就可以尝试TCP、UDP通信了。
TCP/IP协议是一个协议簇。里面包括很多协议,UDP只是其中的一个。
- UDP(User Datagram Protocol用户数据报协议)是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
- UDP 是不具有可靠性的数据报协议。细微的处理它会交给上层的应用去完成。在 UDP 的情况下,虽然可以确保发送消息的大小,却不能保证消息一定会到达。因此,应用有时会根据自己的需要进行重发处理。
1.recvfrom函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
/*
功能:接收数据
参数:
int sockfd:socket函数的返回值,套接字描述符
void *buf:存放收到的数据
size_t len:参数2的大小
int flags:如果没有数据到来 阻塞等待还是不等待 0表示阻塞 MSG_DONTWAIT 不等待
struct sockaddr *src_addr:用于获取发送方的地址信息
socklen_t *addrlen:发送方地址信息长度 注意:传的实参必须初始化
返回值:
成功:返回实际收到的字节数
失败:-1
*/
2.sendto函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
/*
功能:发送数据给对端
参数:
int sockfd:socket函数的返回值,套接字描述符
const void *buf:要发送的数据存放的地址
size_t len:参数2的大小
int flags:套接字缓存满 阻塞还是不阻塞 0表示阻塞 MSG_DONTWAIT 不阻塞
const struct sockaddr *dest_addr:目标端的地址信息
socklen_t *addrlen:目标端的地址信息
返回值:
成功:返回实际发送的字节数
失败:-1
*/
3.示例
实现服务器端与客户端聊天
运行效果:
/***************************/
/* 服务器端 */
/***************************/
#include <stdio.h>
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
char buf_data[1024] = {};
/*创建套接字*/
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd:%dn",sockfd);
/*定义网络地址结构体变量并填充*/
struct sockaddr_in myselfAddr;
myselfAddr.sin_family = AF_INET;
myselfAddr.sin_port = htons(6666);//把短整形转为网络格式
myselfAddr.sin_addr.s_addr = htonl(INADDR_ANY);//主机格式转网络格式
/*套接字与主机绑定*/
int ret_bind = bind(sockfd,(struct sockaddr*)&myselfAddr,sizeof(myselfAddr));
if(ret_bind == -1)
{
perror("bind");
close(sockfd);
exit(1);
}
/*缓存用于获取对端网络地址信息*/
struct sockaddr_in buf_sockaddr;
socklen_t buf_addrlen = sizeof(buf_sockaddr);
printf("等待客户端连接...n");
ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);
if(ret_recv == -1)
{
perror("recvfrom");
close(sockfd);
exit(1);
}
printf("IP:%s:%sn",inet_ntoa(buf_sockaddr.sin_addr),buf_data);
pid_t pid = fork();
if(pid>0)
{
while(1)
{
bzero(buf_data,sizeof(buf_data));
gets(buf_data);
ssize_t ret_send = sendto(sockfd,buf_data,strlen(buf_data)+1,0,(struct sockaddr*)&buf_sockaddr,buf_addrlen);
if(ret_send == -1)
{
perror("sendto");
close(sockfd);
exit(1);
}
printf("我:%sn",buf_data);
}
}
else if(pid == 0)
{
while(1)
{
ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);
if(ret_recv == -1)
{
perror("recvfrom");
close(sockfd);
exit(1);
}
printf("IP:%s:%sn",inet_ntoa(buf_sockaddr.sin_addr),buf_data);
}
}
else
{
perror("fork");
close(sockfd);
exit(1);
}
return 0;
}
/***************************/
/* 客户端 */
/***************************/
#include <stdio.h>
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
char buf_data[1024] = "