Socket 套接字是基于 TCP/IP 的一种封装,是一个抽象层,也就是说提供一套 Socket API 方便我们使用 TCP/IP 端对端的 IO 传输;Socket 编程中最常使用的两种协议,即面向连接的 TCP 协议和无连接的 UDP 协议;TCP/IP 概念层模型:应用层、传输层、网络层、数据链路层;TCP/UDP 是传输层协议主要是提供端对端的接口;IP 协议位于网络层主要是为数据包选择路由;

TCP && UDP 协议

对于 TCP 协议

TCP连接过程:建立连接三次握手、数据传输、断开连接四次挥手;
三次握手:

  1. 客户端向服务端发送连接请求报文段 SYN 包 (SYN=J)。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,等待服务器确认;
  2. 服务端收到连接请求报文段 SYN 包后,如果同意连接,确认客户端的SYN(ACK=J+1),则会发送一个应答,该应答中也会包含自身的数据通讯初始序号 SYN(SYN=K)包也就是 SYN+ACK 包,发送完成后服务器便进入 SYN-RECEIVED 状态。
  3. 当客户端收到连接同意的应答 SYN+ACK 包后,还要向服务端发送一个确认报文 ACK(ACK=K+1) 包。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

四次挥手:

  1. 若客户端认为数据发送完成,则它需要向服务端发送 FIN 的报文连接释放请求,客户端进入等待服务器的响应;
  2. 服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK (ACK=X+1)包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。
  3. 服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求 FIN 的报文,然后服务端便进入 LAST-ACK 状态。
  4. 客户端收到释放请求后,向服务端发送确认应答 ACK,此时客户端进入 TIME-WAIT 状态。该状态会持续 2 MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃)时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。

对于 UDP 协议

UDP 是不需要和 TCP 一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作;

UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显

UDP 提供了单播,多播,广播的功能;

  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

BSD Socket 基本使用相关 API 详解

BSD Socket 是 unix 系统下的 Socket API 可以跨平台使用,API 接口纯 C 语言编写,使用需先导入相关头文件:

1
2
3
4
5
6
// BSD socket 核心函数和数据结构。
#import <sys/socket.h>
// AF_INET 和AF_INET6 地址家族和他们对应的协议家族 PF_INET 和 PF_INET6
#import <netinet/in.h>
// 和IP地址相关的一些函数
#import <arpa/inet.h>

客户端简单实现分如下五个步骤:

1. 创建 socket

创建 socket 套接字调用函数如下:

1
2
3
4
int socket(int domain, int type, int protocol);

// 示例
int socketID = socket(AF_INET, SOCK_STREAM, 0);
  • 第一个参数 domain: AF_INET, 协议域又称协议族(family);常用的协议族有AF_INET( ipv4 )、AF_INET6( ipv6 )、AF_LOCAL(或称 AF_UNIX,Unix 域 Socket)、AF_ROUTE等;协议族决定了 socket 的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 ipv4 地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址;
  • 第二个参数 type: SOCK_STREAM, 指定 Socket 类型;常用的 socket 类型有 SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等;流式Socket( SOCK_STREAM )是一种面向连接的 Socket,针对于面向连接的 TCP 服务应用。数据报式Socket( SOCK_DGRAM )是一种无连接的 Socket,对应于无连接的 UDP 服务应用。
  • 第三个参数 protocol: 0 , 指定协议;常用协议有 IPPROTO_TCP( TCP 传输协议 )、 IPPROTO_UDP( UDP 传输协议 )、IPPROTO_STCP( STCP 传输协议 )、IPPROTO_TIPC(TIPC 传输协议)等;type 和 protocol 不可以随意组合,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 组合。当第三个参数为 0 时,会自动选择第二个参数类型对应的默认协议;
  • 返回值 socketID, 如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET(Linux下失败返回-1);

2. 建立连接

建立连接调用函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);

// 示例
struct in_addr socketIn_addr;
socketIn_addr.s_addr = inet_addr("127.0.0.1");
struct sockaddr_in socketAddr;
socketAddr.sin_family = AF_INET;
socketAddr.sin_port = htons(8029);
socketAddr.sin_addr = socketIn_addr;

int result = connect(socketID, (const struct sockaddr *)&socketAddr, sizeof(socketAddr));
if (result == 0) {
NSLog(@"socket 连接成功");
}else {
NSLog(@"socket 连接失败");
}
  • 第一个参数 sockfd:套接字的描述符;
  • 第二个参数 * addr:指向数据结构 sockaddr 的指针,其中包括目的端口和IP地址也就是服务器端的地址;
  • 第三个参数 addrlen:sockaddr的长度,可以通过sizeof(struct sockaddr)获得;
  • 返回值 int型:成功则返回0,失败返回非0,错误码 GetLastError();

IP 地址的表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// IPv4 地址用一个 32 位整数来表示
struct sockaddr_in {
[object Object]__uint8_t sin_len; // 地址长度
[object Object]sa_family_t sin_family; // IP 地址协议族,必须设为 AF_INET;PF_INET(协议族)
[object Object]in_port_t sin_port; // 端口
[object Object]struct in_addr sin_addr; // 以网络字节排序的 4 字节 IPv4 地址
[object Object]char sin_zero[8]; // 没有实际意义,是为了让 sockaddr_in 与 sockaddr 两个数据结构保持大小相同内存对齐作用
};

struct in_addr {
uint32_t s_addr; // 按照网络字节顺序存储 IP 地址
};

// IPv6 地址用一个 128 位整数来表示
struct sockaddr_in6 {
[object Object]__uint8_t sin6_len; // 地址长度
[object Object]sa_family_t sin6_family; // IP 地址协议族,必须设为 AF_INET6
[object Object]in_port_t sin6_port; // 端口
[object Object]__uint32_t sin6_flowinfo; // 标记连接通信量
[object Object]struct in6_addr sin6_addr; // 以网络字节排序的 6 字节 IPv6 地址
[object Object]__uint32_t sin6_scope_id; // 地址所在的接口索引(范围 ID)
};

struct in6_addr {
[object Object]union {
[object Object][object Object]__uint8_t __u6_addr8[16];
[object Object][object Object]__uint16_t __u6_addr16[8];
[object Object][object Object]__uint32_t __u6_addr32[4];
[object Object]} __u6_addr; /* 128-bit IP6 address */
} in6_addr_t;

htons(8029) 函数执行是将一个无符号短整型的主机数值转换为网络字节顺序,不同 cpu 是不同的顺序 (big-endian 大端模式, little-endian 小端模式);

inet_addr(“127.0.0.1”) 函数执行是将一个点分十进制的 IP 转换成一个长整数型数;每台机器都有自己的本地回环地址,IP 为 127.0.0.1 ,主机名为 localhost。如果 127.0.0.1 ping 不通,则网卡不正常。

3. 发送数据

向服务端发送数据,调用函数如下:

1
2
3
4
5
ssize_t send(int sockfd, const void * buf, size_t size, int flags);

// 示例
const char *msg = [NSString stringWithFormat:@"zerocc 发送消息"].UTF8String;
ssize_t sendLen = send(self.socketID, msg, strlen(msg), 0);
  • 第一个参数 sockfd:一个用于标识已连接套接口的描述符;
  • 第二个参数 * buf:包含待发送数据的缓冲区;
  • 第三个参数 size:缓冲区中数据的长度,strlen() 函数计算发送内容字节长度;
  • 第四个参数 flags:调用执行方式一般为 0;

4. 接收数据

接收服务端数据,调用函数如下:

1
2
3
4
5
ssize_t recv(int sockfd, void * buf, size_t size, int flags);

// 示例
uint8_t buffer[1024];
ssize_t recvLen = recv(self.socketID, buffer, sizeof(buffer), 0);
  • sockfd:客户端socket;
  • buf:接收内容缓冲区地址;
  • size:接收内容缓存区长度;
  • flags:接收方式,0表示阻塞,必须等待服务器返回数据;
  • 返回值,如果成功,则返回读入的字节数,失败则返回SOCKET_ERROR;

5. 关闭连接

断开连接,调用函数如下:

1
2
3
4
int close(int sockfd);

// 示例
close(self.socketID);

服务端简单实现分如下七个步骤

1. 创建 socket (同客户端)

2 绑定地址

绑定一个地址 (ip地址+端口号),用于客户端连接服务器,客户端这步不需要不用指定 ip 地址和端口号,系统会自动分配一个端口号和自身的 ip 地址,服务端调用 bind() 函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
int bind(int sockfd, const struct sockaddr * addr, socklen_t addrlen);

// 示例
struct in_addr socketIn_addr;
socketIn_addr.s_addr = inet_addr("127.0.0.1");
struct sockaddr_in socketAddr;
socketAddr.sin_family = AF_INET;
socketAddr.sin_port = htons(8029);
socketAddr.sin_addr = socketIn_addr;
bzero(&(socketAddr.sin_zero), 8);
// 绑定成功返回值为 0,失败为 -1
int bind_result = bind(self.socketID, (const struct sockaddr *)&socketAddr, sizeof(socketAddr));

3. 监听等待客户端的连接请求

服务端监听 socket 等待客户端的连接请求,调用的 listen() 如下:

1
2
3
4
int listen(int sockfd, int backlog);

// 示例 第二个参数:socket 可以排队的最大连接个数
int listen_result = listen(self.socketID, 5);

4. 接受客户端连接

服务端监听到客户端的连接请求之后,就会调用 accept() 函数去接收连接请求,accept() 函数返回成功后就和客户端建立连接了,accept() 函数如下:

1
2
3
4
5
6
7
int accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen);

// 示例
struct sockaddr_in client_address;
socklen_t address_len;
int client_socket = accept(self.socketID, (struct sockaddr *)&client_address, &address_len);
self.client_socket = client_socket;
  • 第一个参数 sockfd:服务端创建的 socket 套接字描述符;
  • 第二个参数 addr:返回客户端的地址;
  • 第三个参数 addrlen:addr 的长度;
  • 返回值 client_socket:返回的也是 socket 套接字描述符,但是这个描述符是调用 accept 函数后生成的,是和客户端连接成功后的不同于自己创建的;

5. 接收客户端传来的数据(同客户端)

不同于客户端,特别注意这里要使用 2.4 步中返回的 client_socket 套接字描述符

6. 服务端发送数据(同客户端)

不同于客户端,特别注意这里要使用 2.4 步中返回的 client_socket 套接字描述符

7. 关闭连接 (同客户端)

Socket 简单运用实例

1. 简单 demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#import "CCBSDSocketVC.h"
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>

@interface CCBSDSocketVC ()
@property (nonatomic, strong) UIButton *connectBtn;
@property (nonatomic, strong) UITextField *msgTextField;
@property (nonatomic, strong) UIButton *sendBtn;
@property (nonatomic, strong) UITextView *receiveTextView;
@property (nonatomic, strong) NSMutableAttributedString *receiveTextViewAttributeStr;

@property (nonatomic, assign) int socketID;


@end

@implementation CCBSDSocketVC

- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];

[self setupUI];
}

- (void)setupUI {
self.connectBtn = [UIButton buttonWithType:UIButtonTypeCustom];
self.connectBtn.frame = CGRectMake(100, 64+20, 100, 40);
self.connectBtn.backgroundColor = [UIColor redColor];
[self.connectBtn setTitle:@"连接" forState:UIControlStateNormal];
[self.connectBtn addTarget:self action:@selector(connectBtnClicked) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.connectBtn];

self.msgTextField = [[UITextField alloc] initWithFrame:CGRectMake(30, 140, 200, 30)];
self.msgTextField.font = [UIFont systemFontOfSize:14];
self.msgTextField.borderStyle = UITextBorderStyleRoundedRect;
[self.view addSubview:self.msgTextField];

self.sendBtn = [UIButton buttonWithType:UIButtonTypeCustom];
self.sendBtn.frame = CGRectMake(250, 140, 60, 30);
self.sendBtn.backgroundColor = [UIColor redColor];
[self.sendBtn setTitle:@"发送" forState:UIControlStateNormal];
[self.sendBtn addTarget:self action:@selector(sendBtnClicked) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.sendBtn];

self.receiveTextView = [[UITextView alloc] initWithFrame:CGRectMake(30, 200, 300, 300)];
self.receiveTextView.scrollEnabled = YES;
self.receiveTextView.editable = NO;
self.receiveTextView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:self.receiveTextView];
self.receiveTextViewAttributeStr = [[NSMutableAttributedString alloc] init];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 5. 断开连接
if (self.socketID) {
close(_socketID);
[self.connectBtn setTitle:@"连接" forState:UIControlStateNormal];
}
}

- (void)connectBtnClicked {
// 1. 创建socket
_socketID = socket(AF_INET, SOCK_STREAM, 0);

// 2. 建立连接
struct in_addr socketIn_addr;
socketIn_addr.s_addr = inet_addr("127.0.0.1");
struct sockaddr_in socketAddr;
socketAddr.sin_family = AF_INET;
socketAddr.sin_port = htons(8029);
socketAddr.sin_addr = socketIn_addr;

int result = connect(_socketID, (const struct sockaddr *)&socketAddr, sizeof(socketAddr));
if (result == 0) {
NSLog(@"socket 连接成功");
[self.connectBtn setTitle:@"连接成功" forState:UIControlStateNormal];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self receiveMessage];
});
}else {
NSLog(@"socket 连接失败");
}

}

- (void)sendBtnClicked {
const char *msg = self.msgTextField.text.UTF8String;
// 3. 发送数据
ssize_t sendLength = send(self.socketID, msg, strlen(msg), 0);
NSLog(@"发送了:%ld字节 \n %@",sendLength, self.msgTextField.text);
[self showMsg:self.msgTextField.text msgType:0];
self.msgTextField.text = @"";
}

- (void)receiveMessage {
while (1) {
uint8_t buffer[1024];
// 4. 接收数据
ssize_t receiveLength = recv(self.socketID, buffer, sizeof(buffer), 0);
if (receiveLength > 0) {
NSData *receiveData = [NSData dataWithBytes:buffer length:receiveLength];
NSString *receiveStr = [[NSString alloc] initWithData:receiveData encoding:NSUTF8StringEncoding];
NSLog(@"接收了:%ld字节 \n %@",receiveLength, receiveStr);

dispatch_async(dispatch_get_main_queue(), ^{
[self showMsg:receiveStr msgType:1];
});
}
}
}

- (void)showMsg:(NSString *)msg msgType:(int)msgType{
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.headIndent = 20.f;
NSMutableAttributedString *attributedString;
if (msgType == 0) { // 我发送的
attributedString = [[NSMutableAttributedString alloc] initWithString:msg];
paragraphStyle.alignment = NSTextAlignmentRight;
[attributedString addAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:15],
NSForegroundColorAttributeName:[UIColor whiteColor],
NSBackgroundColorAttributeName:[UIColor blueColor],
NSParagraphStyleAttributeName:paragraphStyle} range:NSMakeRange(0, msg.length)];
[attributedString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:@"\n"]];
}else { // 接收到的
attributedString = [[NSMutableAttributedString alloc] initWithString:msg];
[attributedString addAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:15],
NSForegroundColorAttributeName:[UIColor blackColor],
NSBackgroundColorAttributeName:[UIColor whiteColor],
NSParagraphStyleAttributeName:paragraphStyle} range:NSMakeRange(0, msg.length)];
}
[self.receiveTextViewAttributeStr appendAttributedString:attributedString];
[self.receiveTextViewAttributeStr appendAttributedString:[[NSMutableAttributedString alloc] initWithString:@"\n"]];
self.receiveTextView.attributedText = self.receiveTextViewAttributeStr;
}

@end

2. Netcat 的使用创建 TCP/UDP 连接和监听

可以使用man 指令查看指令相关介绍:

1
192:~ zerocc$ man nc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
NAME
nc -- arbitrary TCP and UDP connections and listens

SYNOPSIS
nc [-46AcDCdhklnrtUuvz] [-b boundif] [-i interval] [-p source_port] [-s source_ip_address] [-w timeout]
[-X proxy_protocol] [-x proxy_address[:port]] [--apple-delegate-pid pid] [--apple-delegate-uuid uuid]
[--apple-ext-bk-idle] [--apple-nowakefromsleep] [--apple-ecn mode] [hostname] [port[s]]

DESCRIPTION
The nc (or netcat) utility is used for just about anything under the sun involving TCP or UDP. It can open TCP
connections, send UDP packets, listen on arbitrary TCP and UDP ports, do port scanning, and deal with both IPv4
and IPv6. Unlike telnet(1), nc scripts nicely, and separates error messages onto standard error instead of send-
ing them to standard output, as telnet(1) does with some.

Common uses include:

o simple TCP proxies
o shell-script based HTTP clients and servers
o network daemon testing
o a SOCKS or HTTP ProxyCommand for ssh(1)
o and much, much more

The options are as follows:

-4 Forces nc to use IPv4 addresses only.

-6 Forces nc to use IPv6 addresses only.

-A Set SO_RECV_ANYIF on socket.

-b boundif
Specifies the interface to bind the socket to.

-c Send CRLF as line-ending

-D Enable debugging on the socket.

-C Forces nc not to use cellular data context.
:

使用命令 -l 开启监听模式,代表着为一个服务等待客户端来连接指定的端口。强制 nc 待命连接,当客户端从服务端断开连接后,过一段时间服务端也会停止监听,但通过选项 -k 我们可以强制服务器保持连接并继续监听端口。

端口号:用于标示进程的逻辑地址,不同进程的标示。有效端口为 0 ~ 65535,其中 0 ~ 1024 由系统使用或者保留端口,开发中不要使用 1024 以下的端口。

1
192:~ zerocc$ nc -lk 8029

3. 发送和接收数据测试

客户端发送数据:11、Aaa;

终端服务发送数据:我们、zerocc

图片名称
1
2
3
4
5
6
7
// 终端监听结果和发送数据
192:~ zerocc$ man nc
192:~ zerocc$ man nc
192:~ zerocc$ nc -lk 8029
11Aaa我们
zerocc

1
2
3
4
5
6
7
8
9
10
11
// Xcode log 打印
... CCBlogCode[55724:17841398] socket 连接成功
... CCBlogCode[55724:17841398] [MC] Reading from private effective user settings.
... CCBlogCode[55724:17841398] 发送了:2字节
11
... CCBlogCode[55724:17841398] 发送了:3字节
Aaa
... CCBlogCode[55724:17841474] 接收了:7字节
我们
... CCBlogCode[55724:17841474] 接收了:7字节
zerocc

看到log 应该会有疑问:为啥终端服务发送过来 我们 是 7个字节,正确的应该是一个中文字符 utf-8 对应应该是 3 个字节,原因是因为终端发送带了换行符占了一个字节;

链接地址