Socket套接字
现有计算机网络体系结构有三种划分形式:OSI七层协议、TCP/IP四层协议结构、五层协议结构。具体介绍参考:【嵌入式开发之网络编程】网络分层、OSI七层模型、TCP/IP及五层体系结构
网络的体系结构 (Network Architecture) 是计算机网络的各层及其协议的集合,就是这个计算机网络及其构件所应完成的功能的精确定义(不涉及实现)。
实现 (implementation) 是遵循这种体系结构的前提下,用何种硬件或软件完成这些功能的问题。
几种常见的网络编程接口
- Berkeley UNIX 操作系统定义了一种 API,它又称为套接字接口 (socket interface)
- 微软公司在其操作系统中采用了套接字接口 API,形成了一个稍有不同的 API,并称之为 Windows Socket
- AT&T 为其 UNIX 系统 V 定义了一种 API,简写为 TLI (Transport Layer Interface)
套接字的基本概念
套接字(Socket)就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。
套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。 Socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制。
Socket是面向客户/服务器模型而设计的。通信的一方扮演客户机的角色,另一方扮演服务器的角色。服务器在运行中一直监听套接字指定的传输层端口,并等待着客户机的连接请求。当服务器端收到客户机发来的连接请求以后,服务器会接受客户机的连接请求,双方建立连接后,就可进行数据的传递。
套接字的类型
Socket处于网络协议的传输层套接字可以分为流套接字、数据报套接字和原始套接字3种不同的类型。
- 流式套接字 (SOCK_STREAM) 提供可靠的、面向连接的通信流;它使用TCP,从而保证数据传输的可靠性和顺序性。
- 数据报套接字 (SOCK_DGRAM) 定义了一种不可靠、面向无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP。
- 原始套接字(SOCK_RAW) 允许直接访问底层协议,如IP或ICMP,它功能强大但使用较为不便,主要用于协议开发。
套接字是一种特殊的文件描述符,UNIX域套接字用于进程间通信。
套接字是服务器和客户端各自维护一个“文件”,在建立连接后,向自己文件写入内容供对方读取或者读取对方内容,通信结束时关闭文件。
套接字的地址族
socket通用地址族结构体
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
typedef unsigned short int sa_family_t;
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议 族(protocol family,也称domain)和对应的地址族入下所示:
协议族 | 地址族 | 描述 |
PF_UNIX | AF_UNIX | UNIX本地域协议族 |
PF_INET | AF_INET | TCP/IPv4协议族 |
PF_INET6 | AF_INET6 | TCP/IPv6协议族 |
宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:
协议族 | 地址值含义和长度 |
PF_UNIX | 文件的路径名,长度可达108字节 |
PF_INET | 16 bit端口号和32 bit IPv4地址,共6个字节 |
PF_INET6 | 16 bit端口号、32 bit 流标识和 128 bit IPv6地址,共6个字节,32 bit范围ID,共26字节 |
由此可知,14 字节的sa_data 根本无法容纳多数协议族的地址值。
因此,Linux定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
#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;
socket专用地址族结构体
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是 sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family;
char sun_path[108];
};
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和 IPv6。
用于 IPv4的socket地址结构体 sockaddr_in:
#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;
};
用于 IPv6的socket地址结构体 sockaddr_in6:
#include <netinet/in.h>
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 */
};
struct in6_addr {
uint8_t s6_addr[16];
};
typedef uint32_t in_addr_t;
typedef uint16_t in_port_t;
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
以上内容参考:Linux网络协议之socket(套接字)通信基础
TCP通信的实现
套接字三元组
- IP地址:标识计算机
- 端口号:标识计算机当中的进程
- 协议:指定数据传输的方式
TCP实现的基本框架
TCP服务端实现框架
- socket:创建一个用于监听所有客户端的套接字(是一个特殊的文件描述符)。
- bind:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)。
- listen:设置监听,监听的文件描述符开始工作。
- accept:阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(这个套接字与最开始创建的套接字不同,是另一个文件描述符)。
- read和write:接收数据、发送数据。
- close:通信结束,断开连接,关闭文件。
TCP客户端实现框架
- socket:创建一个用于通信的套接(一个特殊的文件描述符)。
- connect:连接服务器,需要指定连接的服务器的IP和端口
- read和wirte:连接成功了,客户端可以直接和服务器通信,发送或者接收数据。
- close:通信结束,断开连接,关闭文件。
套接字函数
socket函数与通信域
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- 参数:
- domain: 协议族
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0
- SOCK_STREAM : 流式协议默认使用 TCP
- SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值:
- 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1
bind函数与通信结构体
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命 名
- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
示例:为套接字fd绑定通信结构体addr
addr.sin_family = AF_INET;
addr.sin_port = htons(5001);
addr.sin_addr.s_addr = 0;
bind(fd, (struct sockaddr *)&addr, sizeof(addr) );
listen函数
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 5
accept函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符
- -1 : 失败
connect函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端连接服务器
- 参数:
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1
read和write函数
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
实现代码
服务端
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define BACKLOG 5
int main(int argc, const char *argv[])
{
int fd, newfd;
char buf[BUFSIZ] = {};//BUFSIZ 8142
struct sockaddr_in addr;//IPv4 socket专用地址
int ret;
//判断主函数传入参数
if (argc < 3) {
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(0);
}
/*创建套接字
* 使用IPv4互联网协议
* 流式套接字SOCK_STREAM对应协议TCP,所以第三个参数可以为0*/
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(0);
}
/*对地址结构体进行初始化*/
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
#if 0
addr.sin_addr.s_addr = inet_addr(argv[1]);
#else
//IP地址序转换,点分十进制IP转换成网络字节序存储在addr.sin_addr中
if (inet_aton(argv[1], &addr.sin_addr) == 0) {
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
#endif
//地址快速重用
int flag = 1, len = sizeof(int);
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1) {
perror("setsockopt");
exit(1);
}
//绑定通信结构体
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind");
exit(0);
}
//设置套接字为监听模式
if (listen(fd, BACKLOG) == -1) {
perror("listen");
exit(0);
}
//接受客户端的连接请求,生成新的和客户端通信的套接字
if ((newfd = accept(fd, NULL, NULL)) < 0) {
perror("listen");
exit(0);
}
while(1){
memset(buf, 0, BUFSIZ);
ret = read(newfd, buf, BUFSIZ);
if (ret < 0){
perror("read");
exit(0);
} else if (ret == 0) {
break;
} else {
printf("buf = %s\n", buf);
}
}
close(newfd);
close(fd);
return 0;
}
客户端
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define BACKLOG 5
int main(int argc, const char *argv[])
{
int fd;
struct sockaddr_in addr;
char buf[BUFSIZ] = {};
//判断主函数传入参数
if (argc < 3) {
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(0);
}
/*创建套接字
* 使用IPv4互联网协议
* 流式套接字SOCK_STREAM对应协议TCP,所以第三个参数可以为0*/
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(0);
}
//domain和port赋值
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
//IP地址序转换,点分十进制IP转换成网络字节序存储在addr.sin_addr中
if (inet_aton(argv[1], &addr.sin_addr) == 0) {
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
//向服务端发起连接
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("connect");
exit(0);
}
while(1){
printf("Input->");
fgets(buf, BUFSIZ, stdin);
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}
编译Makefile
CC = gcc
CFLAGS = -Wall
all:client server
clean:
rm client server
运行结果
客户端输入:
$ ./client 127.0.0.1 8888
Input->Hello World
Input->
服务器端输出:
$ ./server 0 8888
buf = Hello World
UDP通信的实现
UDP实现的基本框架
UDP服务器实现框架
- socket:创建一个用于监听所有客户端的套接字(是一个特殊的文件描述符)。
- bind:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)。
- revfrom和sendto:接收数据、发送数据。
- close:通信结束,断开连接,关闭文件。
相比TCP通信,UDP通信少了bind和listen环节,传送数据的函数也有所区别。
UDP客户端实现框架
- socket:创建一个用于通信的套接(一个特殊的文件描述符)。
- revfrom和sendto:接收数据、发送数据。
- close:通信结束,断开连接,关闭文件。
相比TCP通信,UDP通信connect环节。
UDP通信中传递数据的函数
UDP 套接字是无连接协议,必须使用 sendto 函数发送数据,必须使用 recvfrom 函数接收数据,发送时需指明目的地址。sendto 函数与 send 功能基本相同, recvfrom 与 recv 功能基本相同,只不过 sendto 函数和 recvfrom 函数参数中都带有对方地址信息,这两个函数是专门为 UDP 协议提供的。
write/read到send/recv
#include <unistd.h>
ssize_t write(int fd, const void buf[.count], size_t count);
-功能:从 buf 指向的缓冲区中写入 count 个字节的数据到 fd 所表示的文件或设备中。
-参数:
fd:文件描述符,表示要写入数据的文件或设备。
buf:一个指向要写入数据的缓冲区的指针。
count:要写入的字节数。
-返回值:成功,返回实际写入的字节数;失败,返回-1,并设置errno
ssize_t read(int fd, void buf[.count], size_t count);
-功能:读取fd对应的文件,并将读取的数据保存到buf,以nbytes为读取单位
-参数:
fd:文件描述符,表示要读取数据的文件或设备。
buf:一个指向要读取数据的缓冲区的指针。
count:要读取的字节数。
-返回值:成功,返回实际读取的字节数;失败,返回-1,并设置errno
#include <sys/socket.h>
ssize_t send(int sockfd, const void buf[.len], size_t len, int flags);
-功能:是一个系统调用函数,用来发送消息到一个套接字中,和sendto,sendmsg功能相似。
-参数:
fd:文件描述符,表示要发送数据的文件或设备。
buf:一个指向要发送数据的缓冲区的指针。
count:要发送的字节数。
flags:下列标志中的0个或多个
MSG_CONFIRM :用来告诉链路层,
MSG_DONTROUTE:不要使用网关来发送数据,只发送到直接连接的主机上。通常只有诊断或者路由程序会使用,这只针对路由的协议族定义的,数据包的套接字没有。
MSG_DONTWAIT :启用非阻塞操作,如果操作阻塞,就返回EAGAIN或EWOULDBLOCK
MSG_EOR :当支持SOCK_SEQPACKET时,终止记录。
MSG_MORE :调用方有更多的数据要发送。这个标志与TCP或者udp套接字一起使用
MSG_NOSIGNAL :当另一端中断连接时,请求不向流定向套接字上的错误发送SIGPIPE ,EPIPE 错误仍然返回。
MSG_OOB:在支持此概念的套接字上发送带外数据(例如,SOCK_STREAM类型);底层协议还必须支持带外数据
-返回值:成功,返回实际发送的字节数;失败,返回-1,并设置errno
ssize_t recv(int sockfd, void buf[.len], size_t len, int flags);
-功能:是一个系统调用函数,用来接收消息到一个套接字中,和recvfrom,recvmsg功能相似。
-参数:
fd:文件描述符,表示要接收数据的文件或设备。
buf:一个指向要接收数据的缓冲区的指针。
count:要接收的字节数。
flags:一个或多个标识
-返回值:成功,返回实际接收的字节数;失败,返回-1,并设置errno
sendto与recvfrom
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void buf[.len], size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len, int flags,
struct sockaddr *_Nullable restrict src_addr,
socklen_t *_Nullable restrict addrlen);
这两个函数的前四个参数同recv/send一样,后两个参数是通信结构体和结构体的宽度。
UDP通信实现代码
服务端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
/* UDP协议的实现过程server端
* 第一步:验证传入参数个数
* 第二步:创建套接字
* 第三步:设置通信结构体
* 第四步:绑定通信结构体
* 第五步:接收客户端数据并打印*/
int main(int argc, const char *argv[])
{
int fd;
struct sockaddr_in addr;
char buf[BUFSIZ] = {};
if (argc < 3){
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(EXIT_FAILURE);
}
/*创建套接字
* 创建SOCK_DGRAM型套接字,对应的协议UDP*/
if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
/*设置通信结构体*/
bzero(&addr, sizeof(addr));
addr.sin_port = htons(atoi(argv[2]));
if (inet_aton(argv[1], &addr.sin_addr) == 0) {
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
/*绑定通信结构体*/
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
while(1) {
bzero(buf, BUFSIZ);
recvfrom(fd, buf, BUFSIZ, 0, NULL, NULL);
printf("buf = %s\n", buf);
}
close(fd);
return 0;
}
客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
/* UDP协议的实现过程client端
* 第一步:验证传入参数的个数
* 第二步:创建套接字
* 第三步:设置通信结构体
* 第四步:向server端发送数据*/
int main(int argc, const char *argv[])
{
int fd;
struct sockaddr_in addr;
char buf[BUFSIZ] = {"Hello World"};
socklen_t addrlen = sizeof(addr);
if (argc < 3){
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(EXIT_FAILURE);
}
/*创建套接字*/
if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
/*设置通信结构体*/
bzero(&addr, sizeof(addr));
/*将传入的端口转换为int型,并转换为大端存储*/
addr.sin_port = htons(atoi(argv[2]));
if (inet_aton(argv[1], &addr.sin_addr) == 0) {
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
while(1) {
bzero(buf, BUFSIZ);
printf("Input->");
fgets(buf, BUFSIZ, stdin);
sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&addr, addrlen);
}
close(fd);
return 0;
}
Makefile文件
CC=gcc
CFLAGS=-Wall
all:server client
clean:
rm server client
运行结果
$ ./server 0 8888
buf = woeoeoeo
buf = woeooe
总结
UDP和TCP的主要区别在于连接性、可靠性、传输效率、连接对象、字节消耗和应用场景。
- 连接性:TCP是面向连接的,意味着在数据传输之前,必须先建立连接。这个过程通常涉及到三次握手,以确保双方都准备好进行通信。而UDP是无连接的,即发送数据之前不需要建立连接,它允许应用程序在没有任何前期设置的情况下发送数据报。
- 可靠性:TCP提供了可靠的数据传输服务,保证了数据的无差错、不丢失、不重复,并且按序到达。它通过使用确认机制、重传机制、校验和等方式来确保数据的完整性和准确性。相比之下,UDP不保证数据的可靠交付,它不会进行数据包的重传,也不保证数据包的顺序,因此被认为是不可靠的数据报协议。
- 传输效率:UDP具有较好的实时性和工作效率,它的首部开销小,只有8个字节,而且不需要建立连接和进行复杂的握手过程,因此传输速度较快。TCP的首部开销较大,至少有20个字节,并且由于它提供了可靠的传输服务,所以在传输效率上通常不如UDP。
- 连接对象:TCP连接只能是点到点的,即一条连接只有两个端点。而UDP支持一对一、一对多、多对一和多对多的交互通信,这使得UDP在某些应用场景下更加灵活。
- 字节消耗:TCP的首部开销较大,至少有20个字节。而UDP的首部开销小,只有8个字节。
- 应用场景:TCP由于其可靠的传输特性,经常用于需要高度数据完整性和准确性的场景,如网页浏览、文件传输等。而UDP由于其传输速度快、开销小的特点,经常用于对实时性要求较高的场景,如在线视频、在线游戏、实时通信等。