每学习一个新的知识的时候,我们都会问这是什么?socket是什么?在各大搜索引擎搜索socket都会从插座给你介绍socket的起源。这些,都不重要!对我们来说,我们只需要知道socket就是可以用于网络设备互相通信的一个工具就可以,我们通常称socket为套接字,知道这些就够了。
写在前面
由于Linux有“一切皆文件”的特性,在socket编程这一块,Linux和Windows的实现是有所出入的,本系列文章默认以Linux为环境。实际上在嵌入式以及物联网领域,更多的还是以Linux为基础的。
socket系列文章大概会分为三篇:
- 基本介绍socket,了解什么是socket,了解socket的基本原理,了解都有哪些类型的socket。顺便会简单介绍IP、MAC和端口,最后写一个简单的socket demo。
- 接着上一节的socket demo中,深入分析socket常用的数据结构,接口函数等基础的语法知识。最后通过了解我最近在做的项目中对socket的使用,来更深入的了解socket的应用。
- socket的高级应用,包括文件传输,域名访问等等。最后会说一说socket中使用udp协议的一些内容。
基本原理
那么,从这里开始,我们正式的开始学习socket。
废话部分:这里我斟酌了很久,甚至写了删,删了再写,如此反复,我就在想,该如何简单的介绍socket通信的原理,让我自己明白,也能让看到这个文章的每一位朋友明白?我还是决定,从socket的分类说起。
socket的分类
由于底层实现的不同,其实socket的实现也是千差万别的。本片文章中主要说说,流式套接字和数据报式套接字吧。可以先说明结论:
- 流式套接字(SOCK_STREAM):基于
TCP
协议实现,熟悉TCP协议的朋友都知道三次握手,这样做的目的是为了数据安全,同时也确保了数据的完整性。但是,这同时也带来了很明显的缺点,那就是慢。以上是流式套接字的特点,而针对流式套接字具体的传输方式,我想再具体说明,所谓流式套接字,就是数据在设备之间有顺序的传输,即“ABCDEFGHIJKLMN”, 必须是按照顺序传输的,不可能先收到B然后再收到A。这样做也是确保数据完整性和安全性的一种实现。
- 数据报式套接字(SOCK_DGRAM):基于
UDP
协议实现,UDP协议可能在编程的过程中使用频率没有TCP协议那么高,UDP最明显的特点就是不需要进行三次握手,只管发送数据,至于客户端能否收到数据,我不管,数据完全与否我也不管。
在本次socket的学习中,主要以基于TCP协议的流式套接字,数据报,后面也会简单的涉及。
流式套接字收发流程
socket通信是CS架构的,服务端和客户端都需要做相应的配置,才能建立通信。在正式开始之前,我希望我们能先大概了解IP,MAC和Port之间的区别:
- IP : 确定你的大体位置,也就是局域网所在位置
- MAC: 确定你的设备是局域网中的哪台设备
- Port: 确定需要连接的进程是设备中的哪一个
首先来看服务端:
-
首先申请一个socket。
在这里插一嘴,在Linux环境中,贯彻了一切皆文件的思想,所以这里的socket实际上也是一个文件,所以申请一个socket,相当于调用open()函数,生成了一个文件描述符,而接下来的所有操作,在底层逻辑上看来就是对文件的各种IO操作。
-
将申请到的socket与IP和Port绑定。
-
服务端开始监听该socket
-
服务端阻塞等到客户端连接
同时,另外一台设备,也就是客户端:
-
申请一个socket。
为什么客户端也要申请一个socket?上面我们提到了服务端创建的socket实际上就是一个文件,相关的操作也是对文件的操作,所以客户端新建一个socket,就是对服务端socket的一个承载。
-
配置要连接的IP和Port
-
连接
-
与服务端进行数据交互
-
交互完成之后关闭套接字
-
客户端流程结束
回到服务端,其实在实际的应用中,服务端的等到客户连接过程是循环等待的。对于程序而言,服务器端的socket资源在使用完毕之后也需要及时释放。
socket demo
最后,我们通过一个特别简单的demo来初次看一下socket的连接过程。
代码段:socket_demo.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int server_socket()
{
int server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建socket,返回值实际上就是一个fd
struct sockaddr_in server_addr; // 配置服务端IP和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("192.168.1.10");
server_addr.sin_port = htons(6666);
bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)); // 将socket和IP以及端口绑定
listen(server_socket, 20); // 开始监听
struct sockaddr_in client_addr;
socklen_t client_addr_size = sizeof(client_addr);
while (1) {
// accept函数会阻塞等待客户端的连接,并返回客户端的socket
int client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_size);
char str[] = "Hello Socket!";
send(client_socket, str, sizeof(str), 0); // 发送Hello Socket给客户端
close(client_socket);
}
close(server_socket);
return 0;
}
int client_socket()
{
int client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("192.168.1.10");
server_addr.sin_port = htons(6666);
connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
char buffer[15];
recv(client_socket, buffer, 15, 0);
printf("Message form server: %s\n", buffer);
close(client_socket);
return 0;
}
代码段:common.h
#ifndef _COMMON_H
#define _COMMON_H
extern int server_socket();
extern int client_socket();
#endif /* _COMMON_H */
上面的demo中,展现了一个socket最基本的连接过程,虽然是最近本的,但是更多高级的,重要的socket用法,都是从基本方法派生的。下一篇文章将,自己讲讲上面每一个函数的具体含义,同时分享我最近项目中需要使用的socket需求。