socket网络编程
通用socket地址
socket地址其实是一个结构体,封装了端口号和IP等信息
socket网络编程中表示socket地址的是结构体sockaddr:
1 | //不能使用ipv6 |
1 | //可以使用ipv6 |
专用socket地址 (IPV4 & IPV6)
1 |
|
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
IP地址转换(字符串ip-整数,主机字节序和网络字节序的转换)
下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:
1 |
|
下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:
1 |
|
实例(ipv4地址转数字)
1 |
|
TCP通信流程
TCP和UDP的比较
- UDP:用户数据报协议,面向无连接,可以单播,多播和广播,面向数据报,不可靠
- TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输
TCP通信基本流程
服务端(Server)
- 服务端创建一个用于监听的套接字
- 将监听套接字描述符和本地的ip地址和端口绑定(服务器地址信息)
- 设置监听,监听的套接字文件描述符开始工作(客户端会向套接字文件缓冲区写数据)
阻塞等待,当有客户端发起连接,解除阻塞,接受客户端连接,会得到一个新的套接字用于通信。
通信:接收数据,发送数据
- 通信结束,断开连接
客户端(Client)
- 创建一个用于通信的套接字
- 连接服务器,需要指定连接的服务器的ip和端口
- 连接成功,客户端可以直接和服务端通信:接收数据和发送数据
- 通信结束,断开连接
socket函数
套接字(socket)是一种抽象的概念,它是计算机网络编程中的一种基本单位。它提供了一种标准的接口,供应用程序通过它来与网络通信。
套接字可以理解为一个“端口”,应用程序通过它来与网络进行通信。当应用程序想要向网络发送数据时,它会将数据发送到指定的套接字;当网络向应用程序发送数据时,它也是通过套接字来进行的。
套接字是一个抽象的概念,它并不具体存在,而是由应用程序在运行时创建和使用。它由两个部分组成:一个是套接字地址(socket address),用于标识通信的主机和端口;另一个是套接字类型,用于指定使用的协议。
简而言之,套接字就是一种接口,用于应用程序与网络进行通信。它提供了一种统一的接口,可以让应用程序与网络通信,而不用关心底层的实现细节。
API
1 |
|
简单的C/S模型代码
server
1 |
|
client
1 |
|
TCP三次握手
TCP是一种面向连接的单播协议,在发送数据之前,通信双方必须在彼此之间建立一条连接,所谓“连接”,其实是客户端和服务器的内存里保存了一份关于对方的信息。如IP地址和端口号等。
TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包I重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在TCP头部。
TCP提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用四次挥手来关闭一个连接。
三次握手发生在客户端连接的时候,当调用connect函数时底层会通过TCP协议进行三次握手。
- 客户端向服务端发送数据,其中 SYN = 1
- 服务端确认收到消息后,发送数据,其中ACK = 1, SYN = 1
- 客户端向服务端发送数据,其中ACK = 1.
三次握手的目的是为了使得C和S都能确定自己和对方的收发都没问题。
TCP滑动窗口
- 发送和接受的速度不一致,造成网络拥塞;
- 可以把窗口理解成缓冲区的大小;
- 通信的双方都有发送缓冲区和接受缓冲区;
- 滑动窗口的本质是接收方每次向发送方发送接收方的缓冲区大小(窗口大小)来控制发送方一次可以发送的数据大小以达到拥塞控制的目的。
TCP四次挥手
- 在通信双方断开连接时进行的操作,客户端和服务端都可以主动发起断开连接
- 断开方发送FIN=1
- 接收方发送ACK=1
- 接收方发送FIN=1
- 发送方发送ACK=1
TCP通信并发
要实现服务器处理并发的任务,需要使用多线程和多进程来解决。
思路:
一个父进程和多个子进程
- 父进程负责接受客户端连接
- 子进程负责通信
简单的多进程C/S实现
server
1 |
|
client
1 |
|
简单的多线程C/S实现
1 |
|
半关闭状态
当TCP链接中A向B发送FIN请求关闭,B回应ACK之后(A进入FIN_WAIT_2状态),并没有立即发送FIN给A, A方则处于半连接状态,此时A可以接收B发送的数据,但是A不能再向B发送数据。从程序的角度,可以使用API来控制半连接状态。
1 |
|
端口复用
- 端口复用最常用的用途是:防止服务器重启时之前绑定的端口还未释放
- 程序突然退出而系统未释放端口
1 |
|
IO多路复用(I/O多路转接)
I/O多路复用使得程序能够同时监听多个文件描述符,能够提高程序的性能,Linux下实现I/O多路复用的系统调用主要有select、poll和epoll。
常见I/O模型
- 阻塞等待:优点是不占用宝贵的CPU时间片,缺点是同一时刻只能处理一个操作。
- 非阻塞,忙轮询:要使用IO多路复用技术。
1. select
主旨思想:
- 构建一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中
- 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行了IO操作,该函数才返回(该函数是阻塞的)
- 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作
1 |
|
程序实例
1 |
|
- select需要每次将文件描述符数组从用户态拷贝到内核态
- O(n) 复杂度的遍历
- select支持的文件描述符太少,默认是1024
- 文件描述符不能重用,每次都要重置
2. poll
poll和select用法大致相同,但是使用了pollfd结构体来封装文件描述符
1 |
|
实例代码
1 |
|
3. epoll
- 调用epoll_create创建一个epoll实例(在内核区,不需要每次从用户区拷贝到内核区)
使用红黑树遍历,效率更高
API
1 |
|
实例代码
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
int main(){
//创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
char ip[] = "127.0.0.1";
inet_pton(AF_INET, ip, &saddr.sin_addr.s_addr);
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1){
perror("bind");
exit(-1);
}
ret = listen(lfd, 8);
//创建一个epoll实例
int epfd = epoll_create(100);
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1){
ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1){
perror("epoll wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++){
int curfd = epevs[i].data.fd;
if(curfd == lfd){
//监听的文件描述符
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}else{
char buf[1024] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1){
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}else if(len > 0){
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}LT模式
。。。
ET模式
。。。
UDP 通信实现
API
1 |
|
代码实例(server & client)
1 |
|
1 |
|