socket学习1

总结学习socket的知识点

什么是Socket

socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。

首先我们知道在Linux中一切皆是文件个概念,所以网络连接也是文件,其和普通的文件一样也有文件描述符。

我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:

  • 用 read() 读取从远程计算机传来的数据;
  • 用 write() 向远程计算机写入数据。

TCP 和 UDP 的端口是互不干扰的,也就是说系统可以同时开启 TCP 80 端口和 UDP 80 端口。

socket 不属于任何一层网络协议,它是对 TCP 层的封装,方便网络编程。

现实生活中,两个人要邮寄信件,必须知道对方的地址。网通信也是如此,只不过这里通信的是程序。程序的地址由三元组(ip 地址,端口,协议)界定。

如果你了解网络协议模型的话,你就会知道,ip 地址是网络层用来路由和通信的标识符,端口(port) 是传输层管理的。而 socket 是在这两层之上,所以需要这两个地址来标识。这里的协议指的是 ipv4,ipv6 或者其他协议。

Socket的种类

Socket有许多种类包括:DARPA Internet 地址(Internet 套接字)、本地节点的路径名(Unix套接字)、CCITT X.25地址(X.25 套接字)

创建 socket 的时候需要指定 socket 的类型,一般有三种:

  • SOCK_STREAM:面向连接的稳定通信,底层是 TCP 协议,我们会一直使用这个。
  • SOCK_DGRAM:无连接的通信,底层是 UDP 协议,需要上层的协议来保证可靠性。
  • SOCK_RAW:更加灵活的数据控制,能让你指定 IP 头部

Internet套接字

主要的传输方式

https://c.biancheng.net/view/2124.html

流格式套接字:SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。

数据报套接字:数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”,在代码中使用 SOCK_DGRAM 表示。
计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。

流格式套接字的特点:

  1. 发送、接受不是同步的
  2. 具有自我修复功能
  3. 按照顺序传输

这个流格式套接字就像传送带一样。

Socket编程中的常用函数



Socket中常用函数之间的关系

Socket()

这个函数用来创建一个套接字,函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int socket(int af, int type, int protocol);
//参数:
//af:表示IP地址类型: 常用的有 AF_INET 和 AF_INET6。AF表示address family地址族 PF表示Protocol协议族。其实这两个是一致的。

//type:表示数据传输方式:常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)

//protocol:表示传输协议,当使用TCP时,IPPROTO_TCP;使用UDP时,IPPROTO_UDP

//返回值:创建的套接字的文件描述符

int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //IPPROTO_TCP表示TCP协议

int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //IPPROTO_UDP表示UDP协议

//上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议,如下所示:
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字

为什么需要第三个参数:一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

https://c.biancheng.net/view/2344.html

bind()函数和connect()函数

bind()

这个函数用来将Socket和相应的ip地址、接口绑定起来。

函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
//参数
//sock:套接字的文件描述符
//addr;包含ip地址和接口号的结构体
//addrlen:结构体的长度

//返回值:0:success -1;error

使用:
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
//将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

sockaddr_in结构体成员:

1
2
3
4
5
6
7
8
9
10
11
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};

in_addr结构体:
struct in_addr{
in_addr_t s_addr; //32位的IP地址
};

sockaddr结构体与sockaddr_in结构体:

为什么要将sockaddr结构体强制转换为sockadr_in结构体:

sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。

sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 IPv6 地址。

connect()函数

函数原型:

1
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); 

listen()函数

开始监听指定的套接字

函数原型:

1
2
3
4
5
6
7
8
9
#include <sys/types.h>        
#include <sys/socket.h>

/*
* sockfd: 监听的 socket 描述符
* backlog: 建立的最大连接数
* return: 成功返回 0,失败返回 -1,并设置 erron
*/
int listen(int sockfd, int backlog);

使用示例:

1
2
3
4
if(listen(fd, 10) == -1){ 
perror("listen error");
return -1;
}

accept()函数

网络编程的核心一步就是建立客户端和服务器端的连接,使用 accept 来建立 2 者的连接:

函数原型:

1
2
3
4
5
6
7
8
9
10
#include <sys/types.h>        
#include <sys/socket.h>

/*
* sockfd: 已经创建的本地正在监听的 socket
* addr: 保存连接的客户端的地址信息
* addrlen: sockaddr 的长度指针
* return: 成功返回客户端的 socket 文件描述符号,失败返回 -1,设置 erron
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
struct sockaddr_in clientaddr;
int clientaddr_len = sizeof(clientaddr);

// 建立连接请求
int client_fd = accept(server_fd, (struct sockaddr *)&clientaddr, &clientaddr_len);
if(client_fd == -1) {
perror("accept error") ;
exit(1) ;
}

// socket fd 使用完毕也必须关闭
close(client_fd);

send()函数和sendto()函数

send()函数

使用 send 函数发送数据,不同于使用 write 函数。send 函数可以进行 flag 的指定,使之控制更加详细。

函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/types.h>        
#include <sys/socket.h>


/*
* sockfd: 已经建立连接的 socket
* buf: 发送的数据
* len: 发送数据的长度
* flags: 发送标志
* return: 成功返回发送的字节数,失败返回 -1,设置 erron
*/
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

/*
* to: 目标地址
* tolen: 目标地址长度
*/
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);

使用示例:

1
2
3
4
5
char* msg = "Hello, Socket";
send(client_fd, msg, strlen(msg), 0);

sendto(client_fd, msg, strlen(msg), 0,
(struct sockaddr*)&dest_addr, sizeof(dest_addr));

recv()函数和recvfrom()函数

既然有发送数据,必然有接收数据的函数,与 send 类似,recv 的功能也跟 read 几乎相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/types.h>        
#include <sys/socket.h>


/*
* sockfd: 已经建立连接的 socket
* buf: 接收的数据
* len: 接收数据的最大长度
* flags: 接收标志
* return: 成功返回接收的字节数,失败返回 -1,设置 erron
*/
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

//recvfrom()从指定的地址接收数据
/*
* from: 接收数据的源地址
* fromlen: 接收数据的源地址长度指针
*/
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);

使用示例:

1
2
3
4
5
6
char buf[1024];
int len = recv(client_fd, buf, sizeof(buf), 0);
if(len == -1) {
perror("recv error") ;
exit(1) ;
}

参考资料


socket学习1
https://ysc2.github.io/ysc2.github.io/2023/11/28/socket学习1/
作者
Ysc
发布于
2023年11月28日
许可协议