├── .gitignore ├── 01. 简单回显服务器 ├── CLOSE_WAIT与TIME_WAIT.md ├── client.go ├── client.js ├── client.py ├── readme.md ├── server.go ├── server.js ├── server.py └── 监听套接字与连接套接字.md ├── 02. 三次握手过程详解 ├── client.c ├── readme.md ├── server.c ├── socket里listen函数的参数backlog的含义.md └── 引用 - 深入探索 Linux listen() 函数 backlog 的含义.md ├── 03. select&epoll模型回显服务器 ├── Python-epoll模型.md ├── Python-select模型.md ├── epoll_server.py ├── readme.md └── select_server.py ├── 04. 协程模型回显服务器 ├── async_client.py ├── async_server.py ├── async_server.py.1 ├── async_server.py.2 ├── client.py ├── readme.md └── socket编程中setblocking()函数的使用.md ├── 05. UDP 简单CS示例 ├── readme.md └── udp_demo.go ├── 06. 发送和接收的分片处理 └── readme.md ├── 07. getsockname与getpeername └── getsockname和getpeername函数.md ├── 08. C语言网络编程 ├── C语言-网络编程(一)基础.md ├── C语言-网络编程(二)并发.md └── 一些认知.md └── ghttpd ├── CHANGES.md ├── README.md ├── htdocs ├── cgi-bin │ └── form.py ├── form.html ├── index.html └── index.py ├── rdsrc ├── 001.png ├── 002.png ├── 003.png └── 004.png └── webserver.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.pyc 3 | *.vscode 4 | .DS_Store -------------------------------------------------------------------------------- /01. 简单回显服务器/CLOSE_WAIT与TIME_WAIT.md: -------------------------------------------------------------------------------- 1 | # CLOSE_WAIT与TIME_WAIT 2 | 3 | 本例中只能实现`CLOSE_WAIT`, 无法实现`TIME_WAIT`. 4 | 5 | 在启动服务端和客户端建立连接后, 将服务端所在终端强制关闭. 端口状态将如下 6 | 7 | ![](https://gitimg.generals.space/cd1cc5696526e3b8ad68fee02f11a5f1.png) 8 | 9 | 其中`FIN_WAIT2`会在一段时间后自动消失, 但是如果客户端没有发送操作, 就会一直保持`CLOSE_WAIT`状态, 程序依然活得好好的. 10 | 11 | 只有当客户端尝试向服务端发送信息, 客户端才会重新刷新socket状态, 此时已经不存在连接, 一般会get到一个`Broken Pipe`的错误. 12 | 13 | ![](https://gitimg.generals.space/69b476f9f8ad86fdf1afc8f3937d7e91.png) 14 | 15 | 看来, 客户端如果意外断开, 服务端将收到空字符串... 16 | 17 | 对于`TIME_WAIT`状态, 本例无法重现. 目前还不清楚`TIME_WAIT`产生的条件. -------------------------------------------------------------------------------- /01. 简单回显服务器/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | import ( 3 | "net" 4 | "log" 5 | "fmt" 6 | ) 7 | 8 | func main(){ 9 | var msg string 10 | // 相当于connect()函数 11 | client, err := net.Dial("tcp", "127.0.0.1:7777") 12 | if err != nil{ 13 | log.Fatal(err) 14 | } 15 | log.Printf("已连接到服务器...") 16 | 17 | buf := make([]byte, 1204) 18 | length, err := client.Read(buf) 19 | if err != nil{ 20 | log.Fatal(err) 21 | } 22 | log.Printf(string(buf[:length])) 23 | 24 | for true{ 25 | // &, 读入指针 26 | fmt.Scanf("%s", &msg) 27 | 28 | client.Write([]byte(msg)) 29 | 30 | if msg == "exit"{ 31 | client.Close() 32 | break 33 | }else{ 34 | length, err := client.Read(buf) 35 | if err != nil{ 36 | log.Fatal(err) 37 | } 38 | log.Printf(string(buf[:length])) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /01. 简单回显服务器/client.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | var readline = require('readline'); 3 | // node的控制台读入操作真是让人火大 4 | 5 | //创建readline接口实例 6 | var rl = readline.createInterface({ 7 | input: process.stdin, 8 | output: process.stdout 9 | }); 10 | 11 | var client = new net.Socket(); 12 | client.connect(7720, '127.0.0.1'); 13 | 14 | client.on('connect', () => { 15 | console.log('connected to server'); 16 | let echo = () => { 17 | rl.question('Input: ', function(answer){ 18 | // 不加close,则不会结束 19 | client.write(answer); 20 | if(answer === 'exit'){ 21 | rl.close(); 22 | client.destroy(); 23 | } 24 | else echo(); 25 | }); 26 | } 27 | echo(); 28 | }); 29 | 30 | client.on('data', (data) => { 31 | console.log('receive from server: ', data.toString()); 32 | }); 33 | 34 | client.on('close', (err) => { 35 | console.log('close'); 36 | client = null; 37 | console.log(client); 38 | }); -------------------------------------------------------------------------------- /01. 简单回显服务器/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!encoding: utf-8 3 | 4 | import socket 5 | 6 | conn_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 7 | conn_fd.connect(('127.0.0.1', 7777)) 8 | print('已连接到服务器...') 9 | 10 | msg = conn_fd.recv(1024) 11 | print(msg) 12 | 13 | while True: 14 | data = input('发送: ') 15 | conn_fd.send(data) 16 | if data == 'exit': 17 | conn_fd.close() 18 | break 19 | else: 20 | msg = conn_fd.recv(1024) 21 | print('收到: ' + msg) 22 | -------------------------------------------------------------------------------- /01. 简单回显服务器/readme.md: -------------------------------------------------------------------------------- 1 | # 一对一 C/S回显服务器 2 | 3 | 就只是一个server, 一个client, 服务端会把客户端发送的消息回显给客户端. 4 | 5 | ## 1. 基本流程 6 | 7 | 1. 服务器启动, 监听指定端口 8 | 9 | 2. 客户端连接, 服务端发送'欢迎'字样. 10 | 11 | 3. 客户端发送任何信息, 服务端都会在收到的信息前加上'Hello, '字样并返回. 12 | 13 | 4. 如果客户端发送'exit', 就关闭本地连接, 同时, 服务端接收到'exit'后也会停止监听. 14 | 15 | 运行情况如下图 16 | 17 | ![](https://gitimg.generals.space/bb140dbf012c9143f02aaad5b8236ec3.png) 18 | 19 | 在这个过程中, 可以看到数据包的如下流向. 20 | 21 | ![](https://gitimg.generals.space/364d910b8c360bebab63cb89c1b1859a.png) 22 | 23 | 三次握手就不说了, 三次握手完成后, 服务端会向客户端主动发送`欢迎!`字样. 正是其中的`Data`字段携带的数据, 用python shell打印其中的16进制数据. 24 | 25 | ```py 26 | >>> print('\xe6\xac\xa2\xe8\xbf\x8e') 27 | 欢迎 28 | ``` 29 | 30 | ------ 31 | 32 | server.go的执行方法 33 | 34 | ``` 35 | [root@localhost tmp]# go run server.go 36 | 2018/03/30 04:29:03 开始监听... 37 | 2018/03/30 04:29:05 收到来自 127.0.0.1:52110 的连接 38 | 2018/03/30 04:29:07 收到来自 127.0.0.1:52110 的信息: general 39 | 2018/03/30 04:29:12 收到来自 127.0.0.1:52110 的信息: jiangming 40 | 2018/03/30 04:29:15 收到来自 127.0.0.1:52110 的信息: exit 41 | 2018/03/30 04:29:15 客户端 127.0.0.1:52110 断开连接... 42 | ``` 43 | -------------------------------------------------------------------------------- /01. 简单回显服务器/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | import ( 3 | "net" 4 | "log" 5 | ) 6 | 7 | func main(){ 8 | var data string 9 | var msg string 10 | listen_fd, err := net.Listen("tcp", "0.0.0.0:7777") 11 | 12 | log.Printf("开始监听...") 13 | 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | 18 | conn_fd, err := listen_fd.Accept() 19 | if err != nil{ 20 | log.Fatal(err) 21 | } 22 | addr := conn_fd.RemoteAddr().String() 23 | log.Printf("收到来自 %s 的连接", addr) 24 | conn_fd.Write([]byte("欢迎!")) 25 | 26 | for true{ 27 | buf := make([]byte, 1024) 28 | length, err := conn_fd.Read(buf) 29 | if err != nil{ 30 | log.Fatal(err) 31 | } 32 | data = string(buf[:length]) 33 | log.Printf("收到来自 %s 的信息: %s", addr, data) 34 | if data == "exit" { 35 | log.Printf("客户端 %s 断开连接...", addr) 36 | conn_fd.Close() 37 | break 38 | } 39 | 40 | msg = "Hello, " + data 41 | conn_fd.Write([]byte(msg)) 42 | } 43 | log.Printf("停止监听...") 44 | listen_fd.Close() 45 | } 46 | -------------------------------------------------------------------------------- /01. 简单回显服务器/server.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | 3 | // nodejs的socket server天生就是多线程的(多客户端同时连接) 4 | // 用户逻辑只要管理好客户端的连接就可以了, 不管是用数组还是字典. 5 | // 建立连接后处理客户端信息. 6 | // 这里的socket参数就是客户端连接 7 | var server = net.createServer(); 8 | server.on('connection', (socket) => { 9 | console.log('client connected'); 10 | // socket 为客户端接入后创建的对象 11 | console.log('服务地址: ', socket.address()); // 输出服务监听的地址 12 | console.log('客户端地址: ', socket.remoteAddress + ':' + socket.remotePort) // 客户端地址 13 | socket.on('data', (data) => { 14 | if(data.toString() === 'exit') socket.destroy(); 15 | console.log('receive from client: ', data.toString()); 16 | }); 17 | // 客户端正常关闭 18 | socket.on('close', (err) => { 19 | console.log('client disconnected'); 20 | }); 21 | // 客户端异常关闭 22 | socket.on('error', (err) => { 23 | console.log(err); 24 | }); 25 | }); 26 | server.on('end', () => { 27 | console.log('client disconnected'); 28 | }); 29 | server.on('error', (err) => { 30 | console.log(err); 31 | }); 32 | server.listen(7720, '0.0.0.0', () => { 33 | console.log('server listening...'); 34 | }); -------------------------------------------------------------------------------- /01. 简单回显服务器/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!encoding: utf-8 3 | 4 | import socket 5 | 6 | listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 7 | listen_fd.bind(('0.0.0.0', 7777)) 8 | 9 | listen_fd.listen(2) 10 | print('开始监听...') 11 | 12 | ## accept是一个阻塞方法, 如果没有客户端连接进入就停在这里 13 | ## addr是一个元组, 格式为 ('客户端IP', 客户端端口) 14 | conn_fd, addr = listen_fd.accept() 15 | print('收到来自 %s 的连接' % addr[0]) 16 | conn_fd.send('欢迎!') 17 | 18 | ## 循环接受客户端发送的消息 19 | while True: 20 | data = conn_fd.recv(1024) 21 | print(data) 22 | if data == 'exit': 23 | print('客户端 %s 断开连接...' % addr[0]) 24 | conn_fd.close() 25 | break 26 | else: 27 | conn_fd.send('Hello, ' + data) 28 | 29 | listen_fd.close() 30 | print('停止监听...') -------------------------------------------------------------------------------- /01. 简单回显服务器/监听套接字与连接套接字.md: -------------------------------------------------------------------------------- 1 | # 关于监听套接字与连接套接字 2 | 3 | 在启动服务端, 再启动一个客户端, 两者建立连接后, 用`netstat`查看连接状态, 可见如下输出. 4 | 5 | ``` 6 | [general@localhost tmp]$ netstat -nap | grep python 7 | (Not all processes could be identified, non-owned process info 8 | will not be shown, you would have to be root to see it all.) 9 | tcp 3 0 0.0.0.0:7777 0.0.0.0:* LISTEN 82550/python 10 | tcp 0 0 127.0.0.1:7777 127.0.0.1:56464 ESTABLISHED 82550/python 11 | tcp 0 0 127.0.0.1:56464 127.0.0.1:7777 ESTABLISHED 82551/python 12 | ``` 13 | 14 | 其中, 处于`LISTEN`状态的, 为`server.py`中的`listen_fd`, 我称之为`监听套接字`. 15 | 16 | 然后, 执行`accept()`后将得到一个`连接套接字`(如果没有客户端连接进来, 则会阻塞), 状态为`ESTABLISHED`, 所以在服务器端, 一般会存在两种套接字. -------------------------------------------------------------------------------- /02. 三次握手过程详解/client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define PORT 8888 13 | #define thread_num 10 //定义创建的线程数量 14 | 15 | struct sockaddr_in serv_addr; 16 | 17 | void *func() 18 | { 19 | int conn_fd; 20 | conn_fd = socket(AF_INET,SOCK_STREAM,0); 21 | printf("conn_fd : %d\n",conn_fd); 22 | 23 | if( connect(conn_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) == -1) { 24 | printf("connect error\n"); 25 | } 26 | 27 | while(1) {} 28 | } 29 | 30 | int main(int argc,char *argv[]) 31 | { 32 | memset(&serv_addr,0,sizeof(struct sockaddr_in)); 33 | serv_addr.sin_family = AF_INET; 34 | serv_addr.sin_port = htons(PORT); 35 | inet_aton("192.168.30.155", (struct in_addr *)&serv_addr.sin_addr); //此IP是局域网中的另一台主机 36 | int retval; 37 | 38 | //创建线程并且等待线程完成 39 | pthread_t pid[thread_num]; 40 | for(int i = 0 ; i < thread_num; ++i) 41 | { 42 | pthread_create(&pid[i],NULL,&func,NULL); 43 | } 44 | 45 | for(int i = 0 ; i < thread_num; ++i) 46 | { 47 | pthread_join(pid[i],(void*)&retval); 48 | } 49 | 50 | return 0; 51 | } -------------------------------------------------------------------------------- /02. 三次握手过程详解/readme.md: -------------------------------------------------------------------------------- 1 | # 三次握手过程详解 2 | 3 | 这次的示例是为了理解系统函数调用的执行过程对应的握手时的状态变化. 4 | 5 | 包括 6 | 7 | 1. `listen()`函数中`backlog`参数的含义. 8 | 9 | 2. `accept()`函数调用前后, 服务端与客户端程序的socket状态. -------------------------------------------------------------------------------- /02. 三次握手过程详解/server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define PORT 8888 //端口号 13 | #define BACKLOG 5 //BACKLOG大小 14 | 15 | void my_err(const char* msg,int line) 16 | { 17 | fprintf(stderr,"line:%d",line); 18 | perror(msg); 19 | } 20 | 21 | 22 | int main(int argc,char *argv[]) 23 | { 24 | int conn_len; 25 | int sock_fd,conn_fd; 26 | struct sockaddr_in serv_addr,conn_addr; 27 | 28 | 29 | if((sock_fd = socket(AF_INET,SOCK_STREAM,0)) == -1) { 30 | my_err("socket",__LINE__); 31 | exit(1); 32 | } 33 | 34 | memset(&serv_addr,0,sizeof(struct sockaddr_in)); 35 | serv_addr.sin_family = AF_INET; 36 | serv_addr.sin_port = htons(PORT); 37 | serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 38 | 39 | 40 | if(bind(sock_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) == -1) { 41 | my_err("bind",__LINE__); 42 | exit(1); 43 | } 44 | 45 | if(listen(sock_fd,BACKLOG) == -1) { 46 | my_err("sock",__LINE__); 47 | exit(1); 48 | } 49 | 50 | conn_len = sizeof(struct sockaddr_in); 51 | 52 | 53 | sleep(10); //sleep 10s之后接受一个连接 54 | printf("I will accept one\n"); 55 | accept(sock_fd,(struct sockaddr *)&conn_addr,(socklen_t *)&conn_len); 56 | 57 | sleep(10); //同理,再接受一个 58 | printf("I will accept one\n"); 59 | accept(sock_fd,(struct sockaddr *)&conn_addr,(socklen_t *)&conn_len); 60 | 61 | sleep(10); //同理,再次接受一个 62 | printf("I will accept one\n"); 63 | accept(sock_fd,(struct sockaddr *)&conn_addr,(socklen_t *)&conn_len); 64 | 65 | 66 | while(1) {} //之后进入while循环,不释放连接 67 | return 0; 68 | } -------------------------------------------------------------------------------- /02. 三次握手过程详解/socket里listen函数的参数backlog的含义.md: -------------------------------------------------------------------------------- 1 | # socket里listen函数的参数backlog的含义 2 | 3 | 参考文章 4 | 5 | 1. [Socket里listen函数的参数含意](http://blog.csdn.net/wuhuiran/article/details/1602126) 6 | 7 | 2. [Socket里listen函数的参数含意](http://m.itboth.com/d/uY7nqe/socket) 8 | 9 | 3. [深入探索 Linux listen() 函数 backlog 的含义](http://blog.csdn.net/yangbodong22011/article/details/60399728) 10 | 11 | 4. [TCP SOCKET中backlog参数的用途是什么?](http://www.cnxct.com/something-about-phpfpm-s-backlog/) 12 | 13 | 5. [关于TCP 半连接队列和全连接队列](http://jm.taobao.org/2017/05/25/525-1/) 14 | 15 | 6. [Socket `accept queue is full ` 但是一个连接需要从SYN->ACCEPT](https://blog.csdn.net/yangbodong22011/article/details/60468820) 16 | 17 | 7. [TCP状态转换图](https://gitimg.generals.space/fb4e26fc71e3559a8599b6dcb10d09ce.jpeg) 18 | 19 | 不管是C还是python或是其他语言, socket编程中都有一个`listen`函数. 20 | 21 | ```c 22 | int listen(int sockfd, int backlog); 23 | ``` 24 | 25 | `listen`接受一个名为`backlog`的参数, 数值类型, 之前一直不明白它有什么含义. 这里记录一下. 26 | 27 | 参考文章1和2较浅显地指出了, `backlog`是一个队列的长度. 但是超过这个个数的客户端在连接时并不会直接被拒绝而断开连接. 28 | 29 | 参考文章3则非常客观地评价了网上的几种说法, 并且根据实验验证了自己的猜测. 30 | 31 | 参考文章4和5的话题就很深入了, 我们一般接触不到. 32 | 33 | 这篇笔记只是验证并记录一下参考文章3的推理过程...^_^ 34 | 35 | ## 1. 观点 36 | 37 | 首先阐明一下观点. 38 | 39 | Linux为每个socket进程维护两个队列, 一般称之为**全连接队列**与**半连接队列**. 40 | 41 | 全连接队列就是被`listen`的`backlog`参数限制长度的队列, 它们全都是`ESTABLISH`状态的. 在服务端程序开启监听后, 前`backlog + 1`个客户端3次握手后能直接与服务端建立连接. 服务端程序每一次调用`accept`函数其实就是从这个队列中取一个连接进行处理的; 42 | 43 | 半连接队列就是没能排在前`backlog + 1`的客户端连接队列, 它们是`SYN_RECV`状态, 值得了解的是, 它们其实也与服务端完成了3次握手过程(与服务端握手不如说是与系统内核协议栈握手), 但是被内核挂起了, 所以客户端只能等待, 客户端程序中的`connect`函数没能返回. 44 | 45 | ~~这个队列的长度也是可控的, 它就是`/proc/sys/net/ipv4/tcp_max_syn_backlog`的值.~~ 46 | 47 | 还有一种就是服务端程序从全连接队列里`accept`了`n`多个连接, 但是一个进程不可能无限制地与服务端建立socket连接, 所以这个`n`也是有限制的, 这个值就是`ulimit -n`, 即打开文件数的值. 48 | 49 | ## 2. 验证listen的backlog 50 | 51 | 实验环境: 52 | 53 | - Fedora 24 54 | - python 2.7 55 | 56 | 我们依然使用之前回显程序的CS代码. `server.py`中`listen`的参数为2, 就是说, 服务端除了有一个`accept`这个正式建立的连接, 最多还可能再开3个`ESTABLISH`状态的连接, 剩下的就只能是`SYN_RECV`的了. 57 | 58 | 启动服务端程序, 再启动4个客户端程序, 其中第一个客户端与服务端建立了连接, 而另外3个则没有进程处理. `netstat`状态如下. 59 | 60 | ![](https://gitimg.generals.space/c2c71a7602589545520aed73d290e2b2.png) 61 | 62 | 由于是在同一台服务器上做的实验, 所以只要看左侧有红色`:7777`的行即可. 排除监听套接字, 还有4行, 其中`127.0.0.1:42112`行后面有`86207/python`, 说明这条连接的处理程序正是我们的主服务端程序, 而另外3行则没有处理程序, 说明它们正在全连接队列中**待命**, 等待被`accept`. 63 | 64 | 然后再启动一个客户端. 我们将得到第一个`SYN_RECV`状态的连接. 65 | 66 | ![](https://gitimg.generals.space/fe46823b37a91ff400aacaa3624e8697.png) 67 | 68 | 同样, 它也是没有被服务端程序处理的, 所以对应行的进程号是空的. 69 | 70 | 在这次开启这个客户端之前我用wireshark抓包观察了一下, 如下图. 71 | 72 | ![](https://gitimg.generals.space/596bf1f3deba10b86bdc325a0a3e9397.png) 73 | 74 | 可以看到, `SYN_RECV`队列中的连接其实也已经完成了3次握手, 但是服务端却在不停地给该客户端发送`[TCP Spurious Retransmission]`包, 导致客户端又得发送3次握手中的最后一个`ACK`包, 于是客户端就被这样挂起了, 实际上客户端的`connect`函数一直没能返回. 75 | 76 | > Spurious: 假的, 欺骗性的 77 | 78 | ## 3. 关于半连接队列的长度 79 | 80 | 上面提到了, 半连接队列的长度也是有限制的, 这个值就是`/proc/sys/net/ipv4/tcp_max_syn_backlog`的值. 在Fedora 24中, 这个值默认为128. 81 | 82 | ``` 83 | general@localhost ~]$ cat /proc/sys/net/ipv4/tcp_max_syn_backlog 84 | 128 85 | ``` 86 | 87 | 现在我们同样测试一下. 保持上节的状态不变, 即一个服务端, 5个客户端. 88 | 89 | 把`tcp_max_syn_backlog`的值修改为2, 我们准备启动第6, 7个客户端了. 90 | 91 | ``` 92 | [root@localhost ~]# sysctl -a | grep tcp_max_syn 93 | net.ipv4.tcp_max_syn_backlog = 128 94 | [root@localhost ~]# sysctl -w net.ipv4.tcp_max_syn_backlog=2 95 | [root@localhost ~]# sysctl -a | grep tcp_max_syn 96 | sysctl: net.ipv4.tcp_max_syn_backlog = 2 97 | ``` 98 | 99 | 这实验有点崩...由于是在同一台服务器上测试, 所以看得可能有点混乱, 这个实验分开测试会比较好. 100 | 101 | 首先, 处于`SYN_RECV`状态的连接不会持久, 一般过一段时间就会"消失", 无法再在服务端查看到, 而服务端程序本身是完全没有察觉的. 这个时间一般是1分钟, 参考文章6文末也有提过. 102 | 103 | 但是在客户端, 会认为已经与服务端建立了连接, 虽然`connect`函数并没有完成, 但是用`netstat`查看时会发现已经是`ESTABLISHED`状态了. (而且服务端异常退出时, 全连接队列的客户端会全部挂掉但半连接队列完全没有反应, 相当于它们只是一厢情愿...). 104 | 105 | 上面的`tcp_max_syn_backlog`参数证明无效, 无论将其值修改为多少, 再同时启动n多个客户端, 服务端上处于`SYN_RECV`的连接最多也只能出现2个. 可以尝试多次修改这个值, 然后查看`SYN_RECV`状态的最大数量, 不会变的. 106 | 107 | 但是如果将`server.py`的`listen`函数`backlog`设置为4, 那最多就可以看到4个处理`SYN_RECV`的连接. 108 | 109 | 最终证明半连接队列的长度同样受到`listen`的backlog参数的影响, 修改这个值, 会同时影响全连接和半连接两个队列, 而全连接比半连接个数多一个. -------------------------------------------------------------------------------- /02. 三次握手过程详解/引用 - 深入探索 Linux listen() 函数 backlog 的含义.md: -------------------------------------------------------------------------------- 1 | # 深入探索 Linux listen() 函数 backlog 的含义 2 | 3 | 原文链接 4 | 5 | [深入探索 Linux listen() 函数 backlog 的含义](http://blog.csdn.net/yangbodong22011/article/details/60399728) 6 | 7 | ## 1. listen()回顾以及问题引入 8 | 9 | listen()函数是网络编程中用来使服务器端开始监听端口的系统调用, 首先来回顾下listen()函数的定义: 10 | 11 | ```c 12 | SYNOPSIS 13 | #include /* See NOTES */ 14 | #include 15 | 16 | int listen(int sockfd, int backlog); 17 | ``` 18 | 19 | 有关于第二个参数含义的问题网上有好几种说法, 我总结了下主要有这么3种: 20 | 21 | 1. Kernel会为`LISTEN`状态的socket维护一个队列, 其中存放`SYN RECEIVED`和`ESTABLISHED`状态的套接字, backlog就是这个队列的大小. 22 | 23 | 2. Kernel会为`LISTEN`状态的socket维护两个队列, 一个是`SYN RECEIVED`状态, 另一个是`ESTABLISHED`状态, 而backlog就是这两个队列的大小之和. 24 | 25 | 3. 第三种和第二种模型一样, 但是backlog是队列`ESTABLISHED`的长度. 26 | 27 | 有关上面说的两个状态`SYN RECEIVED`状态和`ESTABLISHED`状态, 是TCP三次握手过程中的状态转化, 具体可以参考下面的图(在新窗口打开图片): 28 | 29 | ![](https://gitimg.generals.space/6710b615ec46e6bcdee1e998531a4791.jpg) 30 | 31 | ## 2. 正确的解释 32 | 33 | 那上面三种说法到底哪个是正确的呢? 我下面的说法翻译自这个链接: 34 | 35 | [How TCP backlog works in Linux](http://veithen.github.io/2014/01/01/how-tcp-backlog-works-in-linux.html) 36 | 37 | 当一个应用使用`listen`系统调用让socket进入`LISTEN`状态时, 它需要为该套接字指定一个backlog. backlog通常被描述为**连接队列的限制**. 38 | 39 | 由于TCP使用的3次握手, 连接在到达`ESTABLISHED`状态之前经历中间状态`SYN RECEIVED`, 并且可以由`accept`系统调用返回到应用程序. 这意味着TCP/IP堆栈有两个选择来为`LISTEN`状态的套接字实现backlog队列: 40 | 41 | > 备注:一种就是两种状态在同一个队列, 一种是分别在独立的队列. 42 | 43 | 1. 使用**单个队列**实现, 其大小由`listen()`系统调用的backlog参数确定. 当收到`SYN`数据包时, 它发送回`SYN/ACK`数据包, 并将连接添加到队列. 当接收到相应的`ACK`时, 连接将其状态改变为已建立. 这意味着队列可以包含两种不同状态的连接:`SYN RECEIVED`和`ESTABLISHED`. 只有处于后一状态的连接才能通过`accept()`系统调用返回给应用程序. 44 | 45 | 2. 使用**两个队列**实现, 一个`SYN`队列(或半连接队列)和一个`accept`队列(或完整的连接队列). 处于`SYN RECEIVED`状态的连接被添加到`SYN`队列, 并且当它们的状态改变为`ESTABLISHED`时, 即当接收到3次握手中的`ACK`分组时, 将它们移动到`accept`队列. 显而易见, **`accept`系统调用只是简单地从完成队列中取出连接**. 在这种情况下, `listen()`系统调用的backlog参数表示完成队列的大小. 46 | 47 | 历史上, BSD派生系统实现的TCP协议使用第一种方法. 该选择意味着当达到最大backlog时, 系统将不再响应于`SYN`分组发送回`SYN/ACK`分组. 通常, TCP的实现将简单地丢弃`SYN`分组, 使得客户端重试. 48 | 49 | 在Linux上, 是和上面不同的. 如在listen系统调用的手册中所提到的: 50 | 51 | > 在Linux内核2.2之后, socket backlog参数的形为改变了, **现在它指等待accept的完全建立的套接字的队列长度, 而不是不完全连接请求的数量**. 半连接队列的长度可以使用`/proc/sys/net/ipv4/tcp_max_syn_backlog`设置. 52 | 53 | 这意味着当前Linux版本使用上面第二种说法, 有两个队列:具有由系统范围设置指定的大小的SYN队列和应用程序(也就是backlog参数)指定的accept队列. 54 | 55 | OK, 说到这里, 相信backlog含义已经解释的非常清楚了, 下面我们用实验验证下这种说法 56 | 57 | ## 3. 实验验证 58 | 59 | **验证环境** 60 | 61 | RedHat 7 62 | Linux version 3.10.0-514.el7.x86_64 63 | 64 | **验证思路** 65 | 66 | 1. 客户端开多个线程分别创建socket去连接服务端. 67 | 68 | 2. 服务端在`listen`之后, 不去调用`accept`, 也就是不会从已完成队列中取走socket连接. 69 | 70 | 3. 观察结果, 到底服务端会怎么样? 处于`ESTABLISHED`状态的套接字个数是不是就是backlog参数指定的大小呢? 71 | 72 | 我们定义backlog的大小为5: 73 | 74 | ```c 75 | # define BACKLOG 5 76 | ``` 77 | 78 | 看下我系统上默认的SYN队列大小: 79 | 80 | ![](https://gitimg.generals.space/52b492b26fdd7758681dacda6526d873.jpg) 81 | 82 | 也就是我现在两个队列的大小分别是 : 83 | 84 | SYN队列大小:256 85 | ACCEPT队列大小:5 86 | 87 | 我们的服务端程序`server.c`, 客户端程序`client.c`. 88 | 89 | 编译运行程序, 并用netstat命令监控服务端8888端口的情况: 90 | 91 | ``` 92 | $ gcc server.c -o server 93 | $ gcc client.c -o client -lpthread -std=c99 94 | ``` 95 | 96 | ``` 97 | ## root执行, 可以看到所有的进程信息 98 | ## watch -n 1 表示每秒显示一次引号中命令的结果 99 | $ watch -n 1 "netstat -natp | grep 8888" 100 | ``` 101 | 102 | 在两个终端分别执行`server`与`client`. 103 | 104 | 结果如下: 105 | 106 | 首先是`watch`的情况: 107 | 108 | ![](https://gitimg.generals.space/3dc93123ef3fc2691d8ba53dc6731ccb.jpg) 109 | 110 | - 因为我们客户端用10个线程去连接服务器, 因此服务器上有10条连接. 111 | 112 | - 第一行的`./server`状态是`LISTEN`, 这是服务器进程(这个不算). 113 | 114 | - 倒数第三行的`./server`是服务器已经执行了一次`accept`. 115 | 116 | - 6条`ESTABLISHED`状态比我们的`BACKLOG`参数5大1. 117 | 118 | - 剩余的`SYN_RECV`状态即使收到了客户端第三次握手回应的`ACK`也不能成为`ESTABLISHED`状态, 因为`BACKLOG`队列中没有位置. 119 | 120 | 然后过了10s左右, 等到服务器执行了第二个accept之后, 服务器情况如下: 121 | 122 | ![](https://gitimg.generals.space/520e703f26e08c9247d698ec31f4c063.jpg) 123 | 124 | 此时watch监控的画面如下: 125 | 126 | ![](https://gitimg.generals.space/2cc41ad963cb5a76a5f4da4e0eb745c1.jpg) 127 | 128 | - 和上面相比, 服务器再次`accept`之后, 多了一条`./server`的连接. 129 | 130 | - 有一条连接从`SYN_RECV`状态转换到了`ESTABLISHED`状态, 原因是`accept`函数从`BACKlOG`完成的队列中取出了一个连接, 接着有空间之后, `SYN`队列的一个链接就可以转换成`ESTABLISHED`状态然后放入`BACKlOG`完成队列了. 131 | 132 | 好了, 分析到这里, 有关`BACKLOG`的问题已经解决了, 至于继续上面的实验将backlog的参数调大会怎么样呢? 我试过了, 就是`ESTABLISHED`状态的数量也会增大, 值会是`BACKLOG + 1`, 至于为什么是`BACKLOG + 1`呢? ? ? 我也没有搞懂. 欢迎指教. 133 | 134 | 当然, 还有别的有意思的问题是 : 如果ESTABLISHED队列满了, 可是有连接需要从SYN队列转移过来时会发生什么? 135 | 136 | > 一边在喊:让我过来!我满足条件了. 137 | > 一边淡淡的说:过个毛啊, 没看没地方'住'吗? ~ 138 | 139 | 改天再细说吧, 欢迎评论交流~ -------------------------------------------------------------------------------- /03. select&epoll模型回显服务器/Python-epoll模型.md: -------------------------------------------------------------------------------- 1 | # Python-Epoll模型 2 | 3 | 参考文章 4 | 5 | 1. [Epoll 模型简介](http://www.jianshu.com/p/0fb633010296) 6 | 7 | 2. [python selct模块官方文档epoll节](https://docs.python.org/2/library/select.html#epoll-objects) 8 | 9 | 3. [我读过最好的Epoll模型讲解](http://blog.csdn.net/mango_song/article/details/42643971) 10 | 11 | epoll: edge polling 12 | 13 | ```py 14 | #!encoding: utf-8 15 | import socket 16 | import select 17 | 18 | EOL1 = b'\n\n' 19 | EOL2 = b'\n\r\n' 20 | response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' 21 | response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' 22 | response += b'Hello, world!' 23 | 24 | # 创建套接字对象并绑定监听端口 25 | server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 26 | server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 27 | server.bind(('0.0.0.0', 8080)) 28 | server.listen(5) 29 | server.setblocking(0) 30 | 31 | # 创建epoll对象, 并注册socket对象的epoll可读事件(EPOLLIN) 32 | epoll = select.epoll() 33 | epoll.register(server.fileno(), select.EPOLLIN) 34 | 35 | try: 36 | conns = {} 37 | requests = {} 38 | responses = {} 39 | while True: 40 | # 主循环, epoll的系统调用, 一旦有网络IO事件发生, poll调用返回. 41 | events = epoll.poll(1) 42 | # 通过事件通知获得监听的文件描述符, 进而处理 43 | for fileno, event in events: 44 | ## 如果注册的`监听socket`对象可读, 获取连接, 然后向其注册连接的可读事件 45 | if fileno == server.fileno(): 46 | conn, address = server.accept() 47 | conn.setblocking(0) 48 | ## 对`连接socket`也注册事件 49 | epoll.register(conn.fileno(), select.EPOLLIN) 50 | conns[conn.fileno()] = conn 51 | requests[conn.fileno()] = b'' 52 | responses[conn.fileno()] = response 53 | ## 如果是`连接socket`可读, 处理客户端发送的信息, 并注册连接对象可写 54 | elif event & select.EPOLLIN: 55 | ## 暂存请求头信息, 准备打印到日志 56 | requests[fileno] += conns[fileno].recv(1024) 57 | ## 判断是否为http请求(根据请求头的结束标志) 58 | if EOL1 in requests[fileno] or EOL2 in requests[fileno]: 59 | epoll.modify(fileno, select.EPOLLOUT) 60 | ## 打印请求头信息 61 | print('-' * 40 + '\n' + requests[fileno].decode()[:-2]) 62 | ## 如果是`连接socket`可写, 发送数据到客户端 63 | elif event & select.EPOLLOUT: 64 | byteswritten = conns[fileno].send(responses[fileno]) 65 | responses[fileno] = responses[fileno][byteswritten:] 66 | ## http都是短连接, 响应发送完毕后就停止读写(shutdown) 67 | if len(responses[fileno]) == 0: 68 | epoll.modify(fileno, 0) 69 | conns[fileno].shutdown(socket.SHUT_RDWR) 70 | ## 如果发生EPOLLHUP事件, 则把event对应的socket移除监听队列, 并关闭连接. 71 | elif event & select.EPOLLHUP: 72 | epoll.unregister(fileno) 73 | conns[fileno].close() 74 | del conns[fileno] 75 | finally: 76 | epoll.unregister(server.fileno()) 77 | epoll.close() 78 | server.close() 79 | ``` 80 | 81 | 运行这个脚本, 然后curl它, 会得到一个'hello world'的响应. 简单的telnet是不会有结果的, 因为上述代码没有处理纯tcp的请求. -------------------------------------------------------------------------------- /03. select&epoll模型回显服务器/Python-select模型.md: -------------------------------------------------------------------------------- 1 | # Python-select模型 2 | 3 | 参考文章 4 | 5 | 1. [基于python select.select模块通信的实例讲解](http://www.jb51.net/article/124202.htm) 6 | 7 | 2. [Select 模型简介](http://www.jianshu.com/p/edb9ddd51c3d) 8 | 9 | `select`函数会在socket连接建立, `send`和`recv`函数调用时返回. 服务端需要一个死循环就一直读取`select`. 就是说, 先select, 再能`accept`, `send`和`recv`之后, 再执行`select`才能发送/接收. 10 | 11 | 下面的代码是参考文章1中进行改编的, 一个简单的CS程序. 不过参考文章2的更容易理解一点, 至少我在看懂下面代码的70%后再去读参考文章2的示例代码立刻就明白了. 并且参考文章2对select的原理和不足讲述的很清楚, 配合代码看更形象. 12 | 13 | `server.py` 14 | 15 | ```py 16 | #! encoding: utf-8 17 | import socket 18 | import select 19 | import Queue 20 | from time import sleep 21 | 22 | ## 创建socket 23 | server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 24 | server.setblocking(0) 25 | 26 | # Bind the socket to the port 27 | ## 将socket绑定IP与端口 28 | server_address = ('0.0.0.0', 8090) 29 | print ('starting up on %s port %s' % server_address) 30 | server.bind(server_address) 31 | 32 | ## 开始监听 33 | server.listen(5) 34 | 35 | ## 我们需要读取信息的socket列表 36 | inputs = [server] 37 | 38 | ## 我们希望发送响应的socket列表 39 | outputs = [] 40 | 41 | # Outgoing message queues (socket: Queue) 42 | message_queues = {} 43 | 44 | while inputs: 45 | print ('waiting for the next event') 46 | try: 47 | ## `对inputs`中的服务器端socket进行监听. 48 | ## `select.select`会阻塞, 一直到有连接建立. 49 | ## 调用socket的send, recv函数, `select.select`也会有返回. 50 | readable, writable, exceptional = select.select(inputs, outputs, inputs) 51 | except select.error, e: 52 | print(e) 53 | print('maybe ctrl-c...') 54 | break 55 | 56 | # 循环判断是否有客户端连接进来, 当有客户端连接进来时select 将触发 57 | for s in readable: 58 | ## 判断当前触发的是不是服务端对象, 当触发的对象是服务端对象时, 说明有新客户端连接. 59 | if s is server: 60 | ## 当监听socket有返回时表明有连接建立, 服务器端需要accept. 61 | ## accept后, 我们可以得到名为`client_address`的连接socket(也是socket) 62 | connection, client_address = s.accept() 63 | print('connection from', client_address) 64 | connection.setblocking(0) 65 | # 将客户端对象也加入到监听的列表中, 当客户端发送消息时select将触发 66 | inputs.append(connection) 67 | # 为连接的客户端单独创建一个消息队列,用来保存客户端发送的消息 68 | message_queues[connection] = Queue.Queue() 69 | 70 | ## 如果不是server的socket(`监听socket`), 那就是连接客户端的socket(`连接socket`) 71 | else: 72 | ## 由于客户端连接进来时服务端接收客户端连接请求,将客户端加入到了监听列表中(input_list), 73 | ## 客户端发送消息将触发, 所以判断是否是客户端对象触发. 74 | data = s.recv(1024) 75 | 76 | ## 如果关闭了客户端socket, 那么此时服务器端接收到的data就是'' 77 | if data != '': 78 | print ('received "%s" from %s' % (data, s.getpeername())) 79 | # 将收到的消息放入到相对应的socket客户端的消息队列中 80 | message_queues[s].put(data) 81 | # 将需要进行回复的`连接socket`放到output 列表中, 让select监听 82 | if s not in outputs: 83 | outputs.append(s) 84 | else: 85 | # 客户端断开了连接, 将客户端的监听从input列表中移除 86 | print ('closing', client_address) 87 | if s in outputs: 88 | outputs.remove(s) 89 | inputs.remove(s) 90 | s.close() 91 | 92 | # 移除对应socket客户端对象的消息队列 93 | del message_queues[s] 94 | 95 | ## 如果现在没有客户端请求, 也没有客户端发送消息时??? 96 | ## 开始对发送消息列表进行处理, 是否需要发送消息 97 | for s in writable: 98 | try: 99 | message_queue = message_queues.get(s) 100 | send_data = '' 101 | ## 如果消息队列中有消息,从消息队列中获取要发送的消息 102 | if message_queue is not None: 103 | send_data = message_queue.get_nowait() 104 | else: 105 | # 客户端连接断开了 106 | print "has closed " 107 | except Queue.Empty: 108 | # 客户端连接断开了 109 | print "%s" % (s.getpeername()) 110 | outputs.remove(s) 111 | else: 112 | # print "sending %s to %s " % (send_data, s.getpeername) 113 | # print "send something" 114 | ## 如果取出这条消息之后, 消息队列就空了 115 | if message_queue is not None: 116 | s.send(send_data) 117 | else: 118 | print "has closed " 119 | # del message_queues[s] 120 | # writable.remove(s) 121 | # print "Client %s disconnected" % (client_address) 122 | 123 | # # Handle "exceptional conditions" 124 | # 处理异常的情况 125 | for s in exceptional: 126 | print ('exception condition on', s.getpeername()) 127 | # Stop listening for input on the connection 128 | inputs.remove(s) 129 | if s in outputs: 130 | outputs.remove(s) 131 | s.close() 132 | 133 | # Remove message queue 134 | del message_queues[s] 135 | 136 | ## 实际使用要将sleep移除, 这里可以清楚地看到客户端在发送消息之后1秒左右收到回显. 137 | sleep(1) 138 | ``` 139 | 140 | 这里`select`模型的使用方法可能有点别扭, 所有操作(`accept`, `recv`, `send`)之前/后都要调用`select`. 而且是对3种描述符分别遍历. 141 | 142 | `client.py` 143 | 144 | ```py 145 | #! encoding: utf-8 146 | import socket 147 | import sys 148 | reload(sys) 149 | sys.setdefaultencoding('utf-8') 150 | 151 | messages = ['This is the message ', 'It will be sent ', 'in parts ', u'in 中文'] 152 | server_address = ('localhost', 8090) 153 | 154 | ## 同时操作两个socket 155 | socks = [ 156 | socket.socket(socket.AF_INET, socket.SOCK_STREAM), 157 | socket.socket(socket.AF_INET, socket.SOCK_STREAM), 158 | ] 159 | 160 | print ('connecting to %s port %s' % server_address) 161 | 162 | ## 连接到服务器 163 | for s in socks: 164 | s.connect(server_address) 165 | 166 | for index, message in enumerate(messages): 167 | for s in socks: 168 | print ('%s: sending "%s"' % (s.getsockname(), message + str(index))) 169 | s.send(bytes(message + str(index)).decode('utf-8')) 170 | 171 | for s in socks: 172 | data = s.recv(1024) 173 | print ('%s: received "%s"' % (s.getsockname(), data)) 174 | if data != "": 175 | print ('closingsocket', s.getsockname()) 176 | s.close() 177 | ``` -------------------------------------------------------------------------------- /03. select&epoll模型回显服务器/epoll_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!encoding: utf-8 3 | 4 | import socket 5 | import select 6 | import Queue 7 | 8 | listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 9 | listen_fd.bind(('0.0.0.0', 7777)) 10 | 11 | listen_fd.listen(2) 12 | print('开始监听...') 13 | 14 | 15 | # 创建epoll对象, 并注册socket对象的epoll可读事件(EPOLLIN), 相当于托管 16 | epoll = select.epoll() 17 | epoll.register(listen_fd.fileno(), select.EPOLLIN) 18 | 19 | conn_fds = {} 20 | msg_queues = {} 21 | 22 | while True: 23 | try: 24 | ## 主循环, epoll的系统调用, 一旦有网络IO事件发生, poll调用返回. 25 | ## 可能同时返回多个事件(比如多个连接同时有数据传入) 26 | events = epoll.poll(1) 27 | except KeyboardInterrupt as e: 28 | print('用户终止了进程...') 29 | break 30 | except Exception as e: 31 | pass 32 | ## 每个事件对象都包含着发生该事件的描述符, 类似于js中事件回调的event.target 33 | # $('click', 'div', function(event){ 34 | # console.log(event.target); 35 | # }); 36 | for fileno, event in events: 37 | ## 同样判断如果发生此次事件的是监听套接字, 那一定是有新客户端接入 38 | ## 向新客户端发送欢迎信息, 然后为其注册可读事件, 顺便创建该连接的消息队列 39 | if fileno == listen_fd.fileno(): 40 | conn_fd, addr = listen_fd.accept() 41 | print('收到来自 %s:%d 的连接' % (conn_fd.getpeername()[0], conn_fd.getpeername()[1])) 42 | conn_fd.send('欢迎!') 43 | 44 | conn_fds[conn_fd.fileno()] = conn_fd 45 | msg_queues[conn_fd.fileno()] = Queue.Queue() 46 | ## 貌似不能为同一个描述符创建两个事件监听? 47 | ## 只能在读完将监听修改为写监听, 然后写完再改成读监听 48 | epoll.register(conn_fd.fileno(), select.EPOLLIN) 49 | ## epoll.register(conn_fd.fileno(), select.EPOLLOUT) 50 | 51 | ## 如果是`连接套接字`可读事件 52 | elif event & select.EPOLLIN: 53 | data = conn_fds[fileno].recv(1024) 54 | print('收到来自 %s:%d 的消息: %s' % (conn_fds[fileno].getpeername()[0], conn_fds[fileno].getpeername()[1], data)) 55 | ## 客户端断开后, 要移除事件监听, 移除连接和消息映射 56 | if data == 'exit': 57 | print('客户端 %s:%d 断开连接...' % (conn_fds[fileno].getpeername()[0], conn_fds[fileno].getpeername()[1])) 58 | 59 | epoll.unregister(fileno) 60 | conn_fds[fileno].close() 61 | del conn_fds[fileno] 62 | del msg_queues[fileno] 63 | else: 64 | epoll.modify(fileno, select.EPOLLOUT) 65 | msg_queues[fileno].put(data) 66 | 67 | ## 如果是`连接套接字`可写事件 68 | elif event & select.EPOLLOUT: 69 | msg_queue = msg_queues[fileno] 70 | data = msg_queue.get_nowait() 71 | conn_fds[fileno].send('Hello ' + data) 72 | epoll.modify(fileno, select.EPOLLIN) 73 | 74 | ## 如果发生EPOLLHUP事件, 则把event对应的socket移除监听队列, 并关闭连接. 75 | elif event & select.EPOLLHUP: 76 | epoll.unregister(fileno) 77 | conn_fds[fileno].close() 78 | del conn_fds[fileno] 79 | del msg_queues[fileno] 80 | 81 | print('停止监听...') 82 | listen_fd.close() -------------------------------------------------------------------------------- /03. select&epoll模型回显服务器/readme.md: -------------------------------------------------------------------------------- 1 | # select/epoll模型回显服务器 2 | 3 | 参考文章 4 | 5 | 1. [python下的select模块使用 以及epoll与select、poll的区别](https://blog.csdn.net/caizs566205/article/details/51193535) 6 | - select与epoll的区别和联系 7 | 8 | 相当于开多线程处理客户端连接, 但其实是单线程. 9 | 10 | 运行方法, 同简单回显的服务器一样, 直接`python select_server.py`或`python epoll_server.py`, 但是之后可以同时启动多个客户端, 服务端都会生成响应. 注意如果不在同一台服务器上运行的话, 记得修改`client.py`中服务端连接的地址. 11 | 12 | ![](https://gitimg.generals.space/5558f4b6587c06b06272dedcb33b33f1.png) 13 | 14 | 其实传统的多线程处理方式与select与epoll机制最大的区别在于, 前者是一对一与客户端沟通, 后者则是相当于集中式消息分发. -------------------------------------------------------------------------------- /03. select&epoll模型回显服务器/select_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!encoding: utf-8 3 | 4 | import socket 5 | import select 6 | import Queue 7 | 8 | listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 9 | listen_fd.bind(('0.0.0.0', 7777)) 10 | 11 | listen_fd.listen(2) 12 | print('开始监听...') 13 | 14 | inputs, outputs = [listen_fd], [] 15 | msg_queues = {} 16 | 17 | ## 用inputs作循环条件貌似是传统... 18 | ## 只要监听套接字还在, 就说明进程不该停止 19 | while inputs: 20 | try: 21 | ## `对inputs`中的服务器端socket进行监听. 22 | ## `select.select`会阻塞, 一直到有连接建立, 或是有数据传入或传出. 23 | r, w, x = select.select(inputs, outputs, inputs) 24 | except select.error as e: 25 | print(e.message) 26 | break 27 | except KeyboardInterrupt as e: 28 | print('用户终止了进程...') 29 | break 30 | ## inputs列表中如果有描述符可读, 分两种情况 31 | ## 一种是有新连接建立, listen_fd可以执行accept() 32 | ## 一种是有新信息传入, conn_fd可以执行recv()了 33 | for fd in r: 34 | if fd is listen_fd: 35 | conn_fd, addr = fd.accept() 36 | print('收到来自 %s:%d 的连接' % (conn_fd.getpeername()[0], conn_fd.getpeername()[1])) 37 | 38 | ## 将这个连接套接字加入到监听的列表中 39 | inputs.append(conn_fd) 40 | outputs.append(conn_fd) 41 | conn_fd.send('欢迎!') 42 | ## 为这个连接套接字建立消息队列 43 | msg_queues[conn_fd] = Queue.Queue() 44 | else: 45 | data = fd.recv(1024) 46 | print('收到来自 %s:%d 的消息: %s' % (fd.getpeername()[0], fd.getpeername()[1], data)) 47 | ## 这里在接收阶段就处理了 48 | if data == 'exit': 49 | print('客户端 %s:%d 断开连接...' % (fd.getpeername()[0], fd.getpeername()[1])) 50 | del msg_queues[fd] 51 | inputs.remove(fd) 52 | outputs.remove(fd) 53 | fd.close() 54 | else: 55 | ## 把收到的消息放到消息队列中, 在下一个对w的循环中决定是否回复 56 | msg_queues[fd].put(data) 57 | 58 | ## 接下来对消息队列中有消息的描述符进行处理 59 | for fd in w: 60 | msg_queue = msg_queues.get(fd) 61 | if msg_queue is None: 62 | continue 63 | try: 64 | data = msg_queue.get_nowait() 65 | fd.send('Hello ' + data) 66 | except Queue.Empty as e: 67 | pass 68 | 69 | for fd in x: 70 | print ('与 %s:%d 的连接发生异常' % (fd.getpeername()[0], fd.getpeername()[1])) 71 | del msg_queues[fd] 72 | inputs.remove(fd) 73 | outputs.remove(fd) 74 | fd.close() 75 | 76 | print('停止监听...') 77 | listen_fd.close() -------------------------------------------------------------------------------- /04. 协程模型回显服务器/async_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!encoding: utf-8 3 | 4 | import socket 5 | import asyncio 6 | 7 | loop = asyncio.get_event_loop() 8 | 9 | async def echo_client(): 10 | conn_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 11 | conn_fd.setblocking(0) 12 | await loop.sock_connect(conn_fd, ('127.0.0.1', 7777)) 13 | print('已连接到服务器...') 14 | 15 | msg = await loop.sock_recv(conn_fd, 1024) 16 | msg = msg.decode() 17 | print(msg) 18 | 19 | while True: 20 | data = input('发送: ') 21 | await loop.sock_sendall(conn_fd, data.encode()) 22 | if data == 'exit': 23 | conn_fd.close() 24 | break 25 | else: 26 | msg = await loop.sock_recv(conn_fd, 1024) 27 | msg = msg.decode() 28 | print('收到: ' + msg) 29 | 30 | loop.create_task(echo_client()) 31 | loop.run_forever() -------------------------------------------------------------------------------- /04. 协程模型回显服务器/async_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!encoding: utf-8 3 | ## python版本3.5+才有await, async 4 | 5 | import socket 6 | import asyncio 7 | 8 | loop = asyncio.get_event_loop() 9 | 10 | async def handler(conn_fd): 11 | with conn_fd: 12 | await loop.sock_sendall(conn_fd, str.encode('欢迎!')) 13 | ## 循环接受客户端发送的消息 14 | while True: 15 | data = await loop.sock_recv(conn_fd, 1024) 16 | data = data.decode() 17 | print(data) 18 | 19 | if data == 'exit': 20 | print('客户端 %s:%d 断开连接...' % (conn_fd.getpeername()[0], conn_fd.getpeername()[1])) 21 | conn_fd.close() 22 | break 23 | else: 24 | await loop.sock_sendall(conn_fd, 'Hello, '.encode() + data) 25 | 26 | async def echo_server(): 27 | listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 28 | listen_fd.setblocking(0) 29 | listen_fd.bind(('0.0.0.0', 7777)) 30 | listen_fd.listen(2) 31 | print('开始监听...') 32 | 33 | while True: 34 | ## accept是一个阻塞方法, 如果没有客户端连接进入就停在这里 35 | ## addr是一个元组, 格式为 ('客户端IP', 客户端端口) 36 | conn_fd, addr = await loop.sock_accept(listen_fd) 37 | print('收到来自 %s:%d 的连接' % (conn_fd.getpeername()[0], conn_fd.getpeername()[1])) 38 | coroutine = handler(conn_fd) 39 | loop.create_task(coroutine) 40 | 41 | loop.create_task(echo_server()) 42 | loop.run_forever() -------------------------------------------------------------------------------- /04. 协程模型回显服务器/async_server.py.1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!encoding: utf-8 3 | ## python版本3.5+才有await, async 4 | 5 | import socket 6 | import asyncio 7 | 8 | loop = asyncio.get_event_loop() 9 | 10 | async def accepter(listen_fd): 11 | while True: 12 | try: 13 | conn_fd, addr = listen_fd.accept() 14 | return conn_fd, addr 15 | except BlockingIOError as e: 16 | ## print(e) 17 | continue 18 | 19 | async def reader(conn_fd): 20 | while True: 21 | try: 22 | data = conn_fd.recv(1024).decode() 23 | return data 24 | except BlockingIOError as e: 25 | ## print(e) 26 | continue 27 | 28 | async def writer(conn_fd, data): 29 | conn_fd.send(data.encode()) 30 | 31 | async def handler(conn_fd): 32 | ## await writer(conn_fd, str.encode('欢迎!')) 33 | conn_fd.send('欢迎!'.encode()) 34 | ## 循环接受客户端发送的消息 35 | while True: 36 | data = await reader(conn_fd) 37 | print(data) 38 | 39 | if data == 'exit': 40 | print('客户端 %s:%d 断开连接...' % (conn_fd.getpeername()[0], conn_fd.getpeername()[1])) 41 | conn_fd.close() 42 | break 43 | else: 44 | await writer(conn_fd, 'Hello, ' + data) 45 | 46 | async def echo_server(): 47 | listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 48 | listen_fd.setblocking(0) 49 | listen_fd.bind(('0.0.0.0', 7777)) 50 | listen_fd.listen(5) 51 | print('开始监听...') 52 | 53 | while True: 54 | ## accept是一个阻塞方法, 如果没有客户端连接进入就停在这里 55 | ## addr是一个元组, 格式为 ('客户端IP', 客户端端口) 56 | conn_fd, addr = await accepter(listen_fd) 57 | print('收到来自 %s:%d 的连接' % (conn_fd.getpeername()[0], conn_fd.getpeername()[1])) 58 | loop.create_task(handler(conn_fd)) 59 | 60 | loop.create_task(echo_server()) 61 | loop.run_forever() 62 | -------------------------------------------------------------------------------- /04. 协程模型回显服务器/async_server.py.2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!encoding: utf-8 3 | ## python版本3.5+才有await, async 4 | 5 | import socket 6 | import asyncio 7 | 8 | loop = asyncio.get_event_loop() 9 | 10 | async def reader(conn_fd): 11 | while True: 12 | try: 13 | data = conn_fd.recv(1024).decode() 14 | return data 15 | except BlockingIOError as e: 16 | ## print(e) 17 | continue 18 | 19 | async def writer(conn_fd, data): 20 | conn_fd.send(data.encode()) 21 | 22 | async def handler(conn_fd): 23 | ## await writer(conn_fd, str.encode('欢迎!')) 24 | conn_fd.send('欢迎!'.encode()) 25 | ## 循环接受客户端发送的消息 26 | while True: 27 | data = await reader(conn_fd) 28 | print(data) 29 | 30 | if data == 'exit': 31 | print('客户端 %s:%d 断开连接...' % (conn_fd.getpeername()[0], conn_fd.getpeername()[1])) 32 | conn_fd.close() 33 | break 34 | else: 35 | await writer(conn_fd, 'Hello, ' + data) 36 | 37 | async def echo_server(): 38 | listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 39 | listen_fd.setblocking(0) 40 | listen_fd.bind(('0.0.0.0', 7777)) 41 | listen_fd.listen(5) 42 | print('开始监听...') 43 | 44 | while True: 45 | ## accept是一个阻塞方法, 如果没有客户端连接进入就停在这里 46 | ## addr是一个元组, 格式为 ('客户端IP', 客户端端口) 47 | conn_fd, addr = await loop.sock_accept(listen_fd) 48 | print('收到来自 %s:%d 的连接' % (conn_fd.getpeername()[0], conn_fd.getpeername()[1])) 49 | loop.create_task(handler(conn_fd)) 50 | 51 | loop.create_task(echo_server()) 52 | loop.run_forever() 53 | -------------------------------------------------------------------------------- /04. 协程模型回显服务器/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!encoding: utf-8 3 | 4 | import socket 5 | 6 | conn_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 7 | conn_fd.connect(('127.0.0.1', 7777)) 8 | print('已连接到服务器...') 9 | 10 | msg = conn_fd.recv(1024).decode() 11 | print(msg) 12 | 13 | while True: 14 | data = input('发送: ') 15 | conn_fd.send(data.encode()) 16 | if data == 'exit': 17 | conn_fd.close() 18 | break 19 | else: 20 | msg = conn_fd.recv(1024).decode() 21 | print('收到: ' + msg) 22 | -------------------------------------------------------------------------------- /04. 协程模型回显服务器/readme.md: -------------------------------------------------------------------------------- 1 | # 协程模型回显服务器 2 | 3 | 参考文章 4 | 5 | 1. [Python黑魔法 --- 异步IO( asyncio) 协程](https://www.jianshu.com/p/b5e347b3a17c) 6 | 7 | 2. [从 asyncio 简单实现看异步是如何工作的](https://ipfans.github.io/2016/02/simple-implement-asyncio-to-understand-how-async-works/) 8 | 9 | 3. [Low-level socket operations](https://docs.python.org/3.5/library/asyncio-eventloop.html#low-level-socket-operations) 10 | 11 | 参考文章1深入浅出地介绍了协程及其相关概念(loop事件循环, task任务, future结果对象), 层层递进, 容易理解. 相对于`廖雪峰`老师对`async/await`的两篇介绍文章, 更加系统, 且条理更加分明. 12 | 13 | 1. [asyncio](https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001432090954004980bd351f2cd4cc18c9e6c06d855c498000) 14 | 15 | 2. [async/await](https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/00144661533005329786387b5684be385062a121e834ac7000) 16 | 17 | 只不过偏了一点, 并不完全适用我们`echo server`的使用场景. 但是入门的非常棒. 18 | 19 | 参考文章2比较符合我们的项目, 尤其是对于`echo server`的协程模型, 提供了较为底层的代码, 而不是像网上大部分示例中使用`start_server`, `reader`, `writer`, 或是`asyncio.Procotol`这种更高级的工具. 20 | 21 | ------ 22 | 23 | server端`async_server.py`使用了`asyncio`自带的底层封装`sock_accept`, `sock_recv`和`sock_send`, 需要python3.5+. 24 | 25 | ``` 26 | [root@efd527db107f ~]# python server.py 27 | 开始监听... 28 | 收到来自 127.0.0.1:59148 的连接 29 | get 30 | 收到来自 127.0.0.1:59150 的连接 31 | get 32 | ``` 33 | 34 | `client`没什么较大的变化, 额外又写了一个`async_client.py`客户端(...好像没什么用) 35 | 36 | > 除了python3中取消了`raw_input()`函数, byte str与普通str分成了两个不同的类型, 在`recv`与`send`前后需要调用`encode()`或`decode()`进行转换. 37 | 38 | 要使用`sock_accept`这些`asyncio`内置的异步函数, 需要设置`setblocking(False)`. 否则只有第一个客户端能与服务端进行通信, 在这个连接断开之前, 之后的客户端能与服务端建立连接但无法发送信息, 会一直阻塞. 39 | 40 | ------ 41 | 42 | `async_server.py.1`和`async_server.py.2`是我尝试不使用`asyncio`内置函数`sock_accept`这些, 而是自定义接收与发送协程的示例代码, 不过可惜运行不成功. 43 | 44 | `async_server.py.1`甚至不能与客户端建立连接, 改用`sock_accept`后, 能与第一个接入的客户端进行正常通信, 但是之后的客户端还是会被阻塞. 45 | 46 | 个人感觉异步编程精髓在于异步处理流程以及如何定义协程函数, 但是`asyncio`的封装太强, 对更深入理解协程没有太大帮助. -------------------------------------------------------------------------------- /04. 协程模型回显服务器/socket编程中setblocking()函数的使用.md: -------------------------------------------------------------------------------- 1 | # socket编程中setblocking()函数的使用 2 | 3 | 参考文章 4 | 5 | 1. [“socket.error: [Errno 11] Resource temporarily unavailable” appears randomly](https://stackoverflow.com/questions/38419606/socket-error-errno-11-resource-temporarily-unavailable-appears-randomly) 6 | 7 | ```py 8 | socket.setblocking(flag) 9 | ``` 10 | 11 | 如果flag为0, 则将套接字设置为非阻塞模式. 否则, 套接字将设置为阻塞模式(默认值). 12 | 13 | 在非阻塞模式下, 如果`recv()`调用没有发现任何数据或者`send()`调用无法立即发送数据, 那么将引发`socket.error`异常. 14 | 15 | 在阻塞模式下, 这些调用在处理之前都将被阻塞. 16 | -------------------------------------------------------------------------------- /05. UDP 简单CS示例/readme.md: -------------------------------------------------------------------------------- 1 | 之前对udp编程的认识不深, 实验的时候踩了不少坑. 2 | 3 | 首先提一下, 我们可以用`nc`命令对模拟udp的服务端与客户端. 4 | 5 | ``` 6 | nc -ul 0.0.0.0 7777 7 | ``` 8 | 9 | > 注意: mac下如果用localhost, 监听的端口会是ipv6类型的...写udp的客户端程序连接可能会出错... 10 | 11 | 然后客户端可以用如下方式 12 | 13 | ``` 14 | nc -u localhost 7777 15 | ``` 16 | 17 | 但是!!! nc启动的服务端貌似只接受单个客户端的连接!!! 18 | 19 | 我们写的程序只是监听一个端口, 就可以接收数据了. 而且由于不需要建立连接, 所以不存在创建多线程的需求(除非对数据的处理比较耗时, 可以单独开线程处理). 20 | 21 | 而如果有对多个客户端发送信息时, 你需要知道每个目标的地址...所以, 如果你是服务器, 那么每当客户端连接时就需要保存一份客户端的地址列表. 如果你的客户端, 那你需要知道你要发给哪些服务端. -------------------------------------------------------------------------------- /05. UDP 简单CS示例/udp_demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "time" 7 | ) 8 | 9 | func listen(server string, conn *net.UDPConn) { 10 | buf := make([]byte, 1024) 11 | for { 12 | len, client, err := conn.ReadFromUDP(buf) 13 | if err != nil { 14 | panic(err) 15 | } 16 | log.Printf("Server %s got: %s from %+v\n", server, buf[:len], client) 17 | } 18 | } 19 | 20 | /* 21 | * 本程序展示了两种向UDP服务器发送信息的方法, 22 | * 1. 常规的, 由客户端Dial再发送数据 23 | * 2. 服务器端也可以直接向另一个服务器发送数据, 因为不需要Dial, 直接向着一个合法的UDP地址写数据就可以了 24 | */ 25 | func main() { 26 | // 创建TCP服务器的方式, 不能用于创建UDP服务器, Listen()的第一个参数为tcp或unix, 不能是udp 27 | // serverConn2, err := net.Listen("udp", ":1379") 28 | 29 | // 创建UDP服务器的方式 30 | // :0表示随机生成一个端口, udpAddr1也没有确定的值. 31 | // 在服务端监听成功后返回的对象有一个LocalAddr()方法, 32 | // 能够得到这个端口的具体值, TCP里不好这么用. 33 | udpAddr1, err := net.ResolveUDPAddr("udp", ":0") 34 | log.Println("server 1 udp addr: ", udpAddr1) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | serverConn1, err := net.ListenUDP("udp", udpAddr1) 40 | serverAddr1 := serverConn1.LocalAddr().String() // 这个对象里包含了此次监听的端口值 41 | log.Println("server 1 server addr: ", serverAddr1) 42 | go listen(serverAddr1, serverConn1) 43 | 44 | udpAddr2, err := net.ResolveUDPAddr("udp", "127.0.0.1:7777") 45 | if err != nil { 46 | panic(err) 47 | } 48 | serverConn2, err := net.ListenUDP("udp", udpAddr2) 49 | serverAddr2 := serverConn2.LocalAddr().String() // 这个对象里包含了此次监听的端口值 50 | go listen(serverAddr2, serverConn2) 51 | 52 | ////////////////////////////////////////////////////////////////////////////// 53 | 54 | // 第一种发包的形式, 常规方法, 客户端Dial再发送 55 | client, err := net.Dial("udp", serverAddr2) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | msg1 := []byte("hello server2, I am client") 61 | _, err = client.Write(msg1) 62 | if err != nil { 63 | panic(err) 64 | } 65 | 66 | // 第二种发包的形式, 非主流, 服务端对服务端发送... 67 | // 注意: server2对server1发送在本程序内是不可以的, 68 | // 因为udpAddr1的端口值并不确定, 无法路由, serverAddr1可以但是类型和WriteToUDP所需参数类型不匹配. 69 | msg2 := []byte("hello server2, I am server1") 70 | _, err = serverConn1.WriteToUDP(msg2, udpAddr2) 71 | if err != nil { 72 | panic(err) 73 | } 74 | time.Sleep(time.Second * 60) 75 | } 76 | 77 | /* 78 | * 执行结果: 79 | 2018/09/04 16:35:38 server 1 udp addr: :0 80 | 2018/09/04 16:35:38 server 1 server addr: [::]:55496 81 | 2018/09/04 16:35:38 Server 127.0.0.1:7777 got: hello server2, I am client from 127.0.0.1:55396 82 | 2018/09/04 16:35:38 Server 127.0.0.1:7777 got: hello server2, I am server1 from 127.0.0.1:55496 83 | */ 84 | -------------------------------------------------------------------------------- /06. 发送和接收的分片处理/readme.md: -------------------------------------------------------------------------------- 1 | 参考文章 2 | 3 | 1. [Golang 如何从socket读出所有数据](http://www.cnblogs.com/cobbliu/p/4410118.html) -------------------------------------------------------------------------------- /07. getsockname与getpeername/getsockname和getpeername函数.md: -------------------------------------------------------------------------------- 1 | # getsockname和getpeername函数 2 | 3 | 参考文章 4 | 5 | 1. [UNIX网络编程——getsockname和getpeername函数](http://www.educity.cn/linux/1241293.html) 6 | 7 | - `getsockname`一般用于返回与某个套接字关联的本地协议地址. 8 | - `getpeername`一般用于得到与某个套接字关联的对方的地址. 9 | 10 | ## `getsockname`应用场景 11 | 12 | 1. 在没有调用`bind`的TCP客户端, `connect`成功返回后, `getsockname`用于返回由内核赋予该连接的本地IP地址和本地端口号. 13 | 14 | 2. 在TCP服务端以端口号为0调用bind(此时内核会去选择本地临时端口号)后, `getsockname`用于返回由内核赋予的本地**端口号**(只bind成功就行, 不必listen就能调用). 15 | 16 | 3. 在一个以**通配IP**(一般是在多网卡服务器上)地址调用bind的TCP服务端, 与某个客户的连接一旦建立(`accept`成功返回), `getsockname`就可以用于返回由内核赋予该连接的本地IP地址. 在这样的调用中, 套接字描述符参数必须是已连接套接字的描述符, 而不是监听套接字的描述符. 17 | 18 | ## `getpeername`应用场景 19 | 20 | 1. 当一个服务器的是由调用过`accept`的某个进程通过调用exec执行程序时, 它能够获取客户身份的唯一途径便是调用`getpeername`. 21 | 22 | `getpeername`只有在连接建立后才调用, 否则不能正确获得对方的地址和端口, 所以它的参数`sockfd`一般是连接描述符而不是监听描述符. 23 | 24 | ------ 25 | 26 | 没有连接的UDP程序不能调用`getpeername`, 但是可以调用`getsockname`. 和TCP程序一样, 它的地址和端口不是在调用socket时就指定的, 而是在第一次调用`sendto`函数以后. 27 | 28 | 已经连接的UDP, 在调用connect以后, 这2个函数(getsockname, getpeername)都是可以用的. 但是这时意义不大, 因为已经连接(connect)的UDP已经知道对方的地址. -------------------------------------------------------------------------------- /08. C语言网络编程/C语言-网络编程(一)基础.md: -------------------------------------------------------------------------------- 1 | # C语言-网络编程(一) 2 | 3 | ## 网络编程基本C/S程序 4 | 5 | ```c 6 | /* 7 | server.c 8 | 9 | TCP服务端程序, 接受客户端连接后, 可以读取客户端输入字符串, 将其转换为大写后返回, 10 | 并输出客户端IP与端口, 然后关闭此次连接. 11 | 12 | 可以配合linux下的nc/telnet工具完成测试. 13 | 14 | 参考见<> 37.2.1节, 解释很有用 15 | */ 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #define MAXLINE 80 23 | #define SERVER_PORT 12345 24 | 25 | int main(void) 26 | { 27 | struct sockaddr_in server_addr, client_addr; 28 | socklen_t client_addr_len; 29 | 30 | // listen_fd是监听描述符, 用于bind() 31 | // connect_fd是连接描述符, 用于read/write() 32 | int listen_fd, connect_fd; 33 | char buf[MAXLINE]; 34 | char client_addr_str[INET_ADDRSTRLEN]; 35 | int i, n; 36 | 37 | listen_fd = socket(AF_INET, SOCK_STREAM, 0); 38 | 39 | memset(&server_addr, 0, sizeof(server_addr)); 40 | server_addr.sin_family = AF_INET; 41 | server_addr.sin_addr.s_addr = htonl(INADDR_ANY); 42 | server_addr.sin_port = htons(SERVER_PORT); 43 | 44 | bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); 45 | listen(listen_fd, 20); 46 | printf("Accepting connections...\n"); 47 | 48 | while(1) 49 | { 50 | client_addr_len = sizeof(client_addr); 51 | connect_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len); 52 | n = read(connect_fd, buf, MAXLINE); 53 | // 这里可以得到客户端点分十进制形式的IP, inet_ntop()是一个格式转换函数. 54 | inet_ntop(AF_INET, &client_addr.sin_addr, client_addr_str, sizeof(client_addr_str)); 55 | printf("received from %s at port %d\n", client_addr_str, ntohs(client_addr.sin_port)); 56 | 57 | for(i = 0; i < n; i ++) 58 | buf[i] = toupper(buf[i]); 59 | write(connect_fd, buf, n); 60 | close(connect_fd); 61 | } 62 | } 63 | ``` 64 | 65 | 编译方法 66 | 67 | ``` 68 | $ gcc -c -g server.c 69 | $ gcc -o server server.o 70 | ``` 71 | 72 | 解析 73 | ------ 74 | 75 | `connect`与`bind`函数的功能类似, 都是将文件描述符与socket地址对象绑定, 所以它们的参数格式相同. 由于客户端不需要固定的端口号, 因此不必调用`bind()`, 客户端的端口号**由内核自动分配**. 76 | 77 | > 注意, 客户端不是不允许调用`bind()`, 只是没有必要调用`bind()`固定一个端口号, 服务器也不是必须调用`bind()`, 但如果服务器不调用`bind()`, 内核会自动给服务器分配监听端口, 每次启动服务器时端口号都不一样, 客户端要连接服务器就会遇到麻烦. 78 | 79 | `client.c` 80 | 81 | ```c 82 | #include 83 | #include 84 | #include 85 | #include 86 | #include 87 | #include 88 | #define MAXLINE 80 89 | #define SERVER_PORT 12345 90 | 91 | int main(int argc, char *argv[]) 92 | { 93 | struct sockaddr_in server_addr; 94 | char buf[MAXLINE]; 95 | int sockfd, n; 96 | char *msg; 97 | if(argc != 2) 98 | { 99 | fputs("usage: ./client message\n", stderr); 100 | exit; 101 | } 102 | msg = argv[1]; 103 | 104 | sockfd = socket(AF_INET, SOCK_STREAM, 0); 105 | memset(&server_addr, 0, sizeof(server_addr)); 106 | server_addr.sin_family = AF_INET; 107 | inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); 108 | server_addr.sin_port = htons(SERVER_PORT); 109 | 110 | connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); 111 | write(sockfd, msg, strlen(msg)); 112 | n = read(sockfd, buf, MAXLINE); 113 | printf("Response from server:\n"); 114 | // 直接系统调用, 输出到终端标准输出 115 | write(STDOUT_FILENO, buf, n); 116 | printf("\n"); 117 | close(sockfd); 118 | return 0; 119 | } 120 | ``` 121 | 122 | 编译方法 123 | 124 | ``` 125 | $ gcc -c -g client.c 126 | $ gcc -o client client.o 127 | ``` 128 | 129 | C/S程序使用方法: 130 | 131 | 首先运行`server`程序 132 | 133 | ``` 134 | ./server 135 | Accepting connections... 136 | ``` 137 | 138 | 然后执行`client` 139 | 140 | ``` 141 | ./client abcd 142 | Response from server: 143 | ABCD 144 | ``` 145 | 146 | 对应server的输出为 147 | 148 | ``` 149 | ./server 150 | Accepting connections... 151 | received from 127.0.0.1 at port 38622 152 | ``` 153 | 154 | ## 网络编程IP/端口格式转换实践 155 | 156 | 参考文章 157 | 158 | [inet_aton和inet_network和inet_addr三者比较-《别怕Linux编程》之五](http://roclinux.cn/?p=1160) 159 | 160 | 网络编程中所用到的IP赋值操作, 需要目标类型是网络字节序的整数, 而不是对人类有友好的点分十进制形式的字符串(如'127.0.0.1'这种形式), 所以在创建socket对象之前, 我们需要手动将点分十进制字符串转换成网络字节的变量. 161 | 162 | `inet_network()`, `inet_addr()`, `inet_aton()`这三个都是可选方法. 163 | 164 | `inet_addr`和`inet_network`函数都是用于将字符串形式转换为整数形式用的, 两者区别很小, `inet_addr`返回的整数形式是**网络字节序**, 而`inet_network`返回的整数形式是**主机字节序**. 其他地方两者并无二异. 它俩都有一个小缺陷, 那就是当IP是`255.255.255.255`时, 会认为这是个无效的IP地址, 这是历史遗留问题, 其实在目前大部分的路由器上, 这个`255.255.255.255`的IP都是有效的. 165 | 166 | > `inet_network`, 返回的却是主机字节序, 呵呵... 167 | 168 | `inet_aton`函数和上面这俩小子的区别就是在于它认为`255.255.255.255`是有效的, 它不会冤枉这个看似特殊的IP地址, 它返回的也是网络字节序的IP. `a` to `n`可以看成是'ASCII' -> 'NETWORK'的类型转换. 169 | 170 | ```c 171 | #include 172 | #include 173 | #include 174 | #include 175 | #include 176 | #include 177 | 178 | int main() 179 | { 180 | char str[] = "255.255.255.255"; 181 | char str2[] = "255.255.255.254"; 182 | //inet_addr(), inet_network()与inet_aton()的返回值类型, 都是in_addr_t 183 | in_addr_t net_addr, net_addr2; 184 | struct sockaddr_in server_addr; 185 | 186 | net_addr = inet_addr(str); 187 | net_addr2 = inet_addr(str2); 188 | if(net_addr == -1) 189 | { 190 | printf("Error while executing inet_addr() when IP string is %s\n", str); 191 | } 192 | else 193 | { 194 | printf("net_addr: ip = %lu when IP string is %s\n", ntohl(net_addr), str); 195 | } 196 | if(net_addr2 == -1) 197 | { 198 | printf("Error while executing inet_addr() when IP string is %s\n", str2); 199 | } 200 | else 201 | { 202 | printf("net_addr2: ip = %lu when IP string is %s\n", ntohl(net_addr2), str2); 203 | } 204 | //////////////////////////////////////////////////////////////////////////////////// 205 | net_addr = inet_network(str); 206 | net_addr2 = inet_network(str2); 207 | if(net_addr == -1) 208 | { 209 | printf("Error while executing inet_network() when IP string is %s\n", str); 210 | } 211 | else 212 | { 213 | printf("net_addr: ip = %lu when IP string is %s\n", ntohl(net_addr), str); 214 | } 215 | if(net_addr2 == -1) 216 | { 217 | printf("Error while executing inet_network() when IP string is %s\n", str2); 218 | } 219 | else 220 | { 221 | printf("net_addr2: ip = %lu when IP string is %s\n", ntohl(net_addr2), str2); 222 | } 223 | //////////////////////////////////////////////////////////////////////////////////// 224 | net_addr = inet_aton(str, &server_addr.sin_addr); 225 | net_addr = inet_aton(str, &server_addr.sin_addr); 226 | if(net_addr == -1) 227 | { 228 | printf("Error while executing inet_aton() when IP string is %s\n", str); 229 | } 230 | else 231 | { 232 | printf("net_addr: ip = %lu when IP string is %s\n", ntohl(net_addr), str); 233 | //printf("server_addr.sin_addr"); 234 | } 235 | if(net_addr2 == -1) 236 | { 237 | printf("Error while executing inet_aton() when IP string is %s\n", str2); 238 | } 239 | else 240 | { 241 | printf("net_addr2: ip = %lu when IP string is %s\n", ntohl(net_addr2), str2); 242 | } 243 | } 244 | ``` 245 | 246 | 数值转换嘛, 不算太难. '255.255.255.254'大概是'11111111 11111111 11111111 11111110', 转换十进制整数为'4294967294', 与上述代码的输出一样. 247 | 248 | 能从点分十进制转换成网络字节序形式的整数, 就能再转回来, 主要是`inet_ntoa()`这个函数. 使用示例如下 249 | 250 | ```c 251 | #include 252 | #include 253 | #include 254 | #include 255 | #include 256 | 257 | int main() 258 | { 259 | char *str_addr; 260 | struct sockaddr_in server_addr; 261 | server_addr.sin_addr.s_addr = htonl(4294967294); 262 | str_addr = inet_ntoa(server_addr.sin_addr); 263 | printf("IP addr is: %s while it's net order is 4294967294\n", str_addr); 264 | } 265 | ``` 266 | 267 | 你将得到'255.255.255.254', 与上面代码实验中结果相对应. 268 | 269 | ------ 270 | 271 | 下面两个函数更新一些, 对IPv4地址和IPv6地址都能处理, 而且修正了`inet_addr`的一些缺陷. 272 | 273 | - `inet_pton()`: 将点分十进制字符串('127.0.0.1'形式)转换成网络字节序二进制值 274 | - `inet_ntop()`: 和`inet_pton`函数正好相反, inet_ntop函数是将网络字节序二进制值转换成点分十进制串. 275 | 276 | 下面是`inet_pton()`与`inet_ntop()`函数的示例. 277 | 278 | ```c 279 | #include 280 | #include 281 | #include 282 | #include 283 | #include 284 | #include 285 | int main(int argc, char **argv) 286 | { 287 | struct sockaddr_in sockaddr_obj; 288 | // 一个ip地址变量其实是sockaddr_in结构体中的sin_addr成员变量 289 | // 它转换成字符串形式所占用的大小是INET_ADDRSTRLEN宏的值, 一般为16 290 | char *str_addr="172.16.10.196"; 291 | unsigned long net_addr = 0xC40A10AC; 292 | char str_addr_after[16]; 293 | int result_code; 294 | 295 | //INADDR_ANY宏表示inet_addr("0.0.0.0"), 在头文件中有定义 296 | //服务器端程序绑定了这个, 就相当于绑定了0.0.0.0网卡 297 | printf("INADDR_ANY: 0x%x\n", INADDR_ANY); 298 | printf("inet_addr(0.0.0.0)的值为: 0x%x\n", inet_addr("0.0.0.0")); 299 | printf("INADDR_ANY in sockaddr_in.sin_addr.s_addr: 0x%x\n", htonl(INADDR_ANY)); 300 | //字符串 -> 网络字节 301 | //转换出来的s_addr是网络字节序的 302 | result_code = inet_pton(AF_INET, str_addr, &sockaddr_obj.sin_addr); 303 | printf("%s对应的网络字节序: %2X\n", str_addr, sockaddr_obj.sin_addr.s_addr); 304 | 305 | //网络字节 -> 字符串 306 | //sizeof(struct sockaddr_in) 307 | result_code = inet_ntop(AF_INET, &sockaddr_obj.sin_addr, str_addr_after, 16); 308 | printf("0x%x对应的点分十进制字符串表示: %s\n", net_addr, str_addr_after); 309 | } 310 | ``` 311 | 312 | ## 结构体/变量关系梳理 313 | 314 | ```c 315 | struct sockaddr_in server_addr; 316 | memset(&server_addr, 0, sizeof(server_addr)); 317 | server_addr.sin_family = AF_INET; 318 | server_addr.sin_addr.s_addr = htonl(INADDR_ANY); 319 | server_addr.sin_port = htons(SERVER_PORT); 320 | 321 | bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); 322 | ``` 323 | 324 | 关于`sockaddr_in`和`sockaddr`, 前者的`_in`表示Internet, 网络地址结构, 与之对应的还有一种叫做`sockaddr_un`, 即'Unix Domain Socket', 它们两个都可以使用`sockaddr`这个比较通用的地址结构表示. 325 | 326 | 所以`sockaddr_in`和`sockaddr`是并列的结构, 指向`sockaddr_in`的结构体的指针也可以指向 327 | sockadd的结构体, 并代替它. 实际上, 在网络编程过程中所用到的函数, 接受的参数大多都是`sockaddr`类型的. 你可以使用sockaddr_in建立你所需要的信息, 在最后进行类型转换就可以了. 328 | 329 | 然后 330 | 331 | - `sin_family`: 指代协议族, 在socket编程中只能是AF_INET 332 | - `sin_port`: 存储端口号(使用网络字节序) 333 | - `sin_addr`: 存储IP地址, 类型是`in_addr`结构体. 它的`s_addr`成员变量用来按照网络字节序存储IP地址. 334 | - `sin_zero`: 为了让`sockaddr`与`sockaddr_in`两个数据结构保持大小相同而保留的空字节. 335 | 336 | ## 关于网络字节序与主机字节序的类型转换 337 | 338 | 一共有4个这样的函数: `htons`, `htonl`, `ntohs`, `ntohl`. 339 | 340 | 其中n表示network(网络字节序形式), h表示host(主机字节序形式). 341 | 342 | s表示short类型, l表示long类型(都是无符号整数). 343 | 344 | socket对象的IP/端口的值转换成主机字节序, 可以方便的输出, 注意`inet_aton`/`inet_ntoa`它们接受的参数类型, 是主机字节序还是网络字节序. 345 | 346 | 另外, 一般`htons`/`ntohs`用于端口转换, `ntohl`/`htonl`用于IP转换...反正我是这么理解的. 347 | 348 | 当前系统的主机字节序与网络字节序一致时, 它们相当于空函数...貌似. -------------------------------------------------------------------------------- /08. C语言网络编程/C语言-网络编程(二)并发.md: -------------------------------------------------------------------------------- 1 | # C语言网络编程(二)并发 2 | 3 | fork: 每accept到一个socket之后,开启一个子进程来负责收发处理工作。 4 | 5 | select: 监控文件描述符事件 6 | 7 | epoll: 监控文件描述符事件,比select性能优异,可最大支持2W个连接,有死连接时处理能力高 8 | 9 | ## 1. fork方法 10 | 11 | ```c 12 | /* 13 | server.c 14 | */ 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #define SERVER_PORT 1234 22 | #define BUF_SIZE 1024 23 | 24 | /* 25 | process_client: 客户端请求处理程序 26 | */ 27 | int process_client(int connect_fd) 28 | { 29 | char msg[BUF_SIZE]; 30 | int msg_len; 31 | memset(msg, 0, BUF_SIZE); 32 | 33 | while(1) 34 | { 35 | msg_len = read(connect_fd, msg, BUF_SIZE); 36 | if(msg_len < 0) 37 | { 38 | perror("read() error"); 39 | return -1; 40 | } 41 | //printf("compare result: %d\n", strncmp(msg, "EOF", 3)); 42 | if(strncmp(msg, "EOF", 3) == 0) 43 | { 44 | printf("disconnected...\n"); 45 | close(connect_fd); 46 | return 0; 47 | } 48 | int i; 49 | for(i = 0; i < msg_len; i ++) 50 | msg[i] = toupper(msg[i]); 51 | write(connect_fd, msg, msg_len); 52 | } 53 | 54 | close(connect_fd); 55 | return 0; 56 | } 57 | 58 | int main() 59 | { 60 | int listen_fd, connect_fd; 61 | struct sockaddr_in server_addr, client_addr; 62 | socklen_t client_addr_len; 63 | char client_addr_str[INET_ADDRSTRLEN]; 64 | 65 | listen_fd = socket(AF_INET, SOCK_STREAM, 0); 66 | if(listen_fd < 0) 67 | { 68 | perror("socket() error"); 69 | return -1; 70 | } 71 | 72 | memset(&server_addr, 0, sizeof(server_addr)); 73 | server_addr.sin_family = AF_INET; 74 | server_addr.sin_port = htons(SERVER_PORT); 75 | server_addr.sin_addr.s_addr = htonl(INADDR_ANY); 76 | 77 | if(bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) 78 | { 79 | perror("bind() error"); 80 | return -1; 81 | } 82 | 83 | if(listen(listen_fd, 20) != 0) 84 | { 85 | perror("listen_fd() error"); 86 | return -1; 87 | } 88 | printf("Accepting connections...\n"); 89 | 90 | while(1) 91 | { 92 | client_addr_len = sizeof(client_addr); 93 | connect_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len); 94 | if(connect_fd < 0) 95 | { 96 | perror("accept() error"); 97 | //这里没有退出, 而是直接进入下一次循环, 接受其他请求. 98 | continue; 99 | } 100 | memset(client_addr_str, 0, sizeof(INET_ADDRSTRLEN)); 101 | inet_ntop(AF_INET, &client_addr.sin_addr, client_addr_str, &client_addr_len); 102 | printf("connected from %s, socket id: %d\n", client_addr_str, connect_fd); 103 | 104 | int child_pid = fork(); 105 | if(child_pid < 0) 106 | { 107 | perror("fork() error"); 108 | break; 109 | } 110 | else if(child_pid > 0) 111 | continue; 112 | else{ 113 | process_client(connect_fd); 114 | return 0; 115 | } 116 | } 117 | close(listen_fd); 118 | return 0; 119 | } 120 | ``` 121 | 122 | 上面的代码可以通过在客户端传输"EOF"字符串告知服务器端结束子进程, 但没办法解析到客户端断开事件, 这种情况下子进程无法退出. 解决方法是使用`recv`代替`read`方法. 123 | 124 | 示例代码如下 125 | 126 | ```c 127 | #include 128 | #include 129 | #include 130 | #include 131 | #include 132 | #include 133 | 134 | #define SERVER_PORT 1234 135 | #define BUF_SIZE 1024 136 | 137 | /* 138 | process_client: 客户端请求处理程序 139 | */ 140 | int process_client(int connect_fd) 141 | { 142 | char msg[BUF_SIZE]; 143 | //int msg_len; 144 | ssize_t msg_len; 145 | memset(msg, 0, BUF_SIZE); 146 | 147 | while(1) 148 | { 149 | msg_len = recv(connect_fd, msg, BUF_SIZE, 0); 150 | if(msg_len <= 0) 151 | { 152 | printf("client disconnect...\n"); 153 | close(connect_fd); 154 | return -1; 155 | } 156 | //printf("compare result: %d\n", strncmp(msg, "EOF", 3)); 157 | if(strncmp(msg, "EOF", 3) == 0) 158 | { 159 | printf("disconnected...\n"); 160 | close(connect_fd); 161 | return 0; 162 | } 163 | int i; 164 | for(i = 0; i < msg_len; i ++) 165 | msg[i] = toupper(msg[i]); 166 | write(connect_fd, msg, msg_len); 167 | } 168 | 169 | close(connect_fd); 170 | return 0; 171 | } 172 | ... 173 | ``` 174 | 175 | 处理子进程退出事件, 添加到main()函数的while循环之间. 176 | 177 | ```c 178 | void handle_sigcld(int signo) 179 | { 180 | int pid,status; 181 | pid = waitpid( -1, &status, 0);//-1表示等待任何子进程 182 | //printf("child process %d exit with %d\n",pid,status); 183 | } 184 | ``` -------------------------------------------------------------------------------- /08. C语言网络编程/一些认知.md: -------------------------------------------------------------------------------- 1 | 关于`send`和`recv` 2 | 3 | `recv`是阻塞方法, 如果缓冲区中没有消息, 它会阻塞. 而它一旦调用, 就会从缓冲区中取走最多指定字节的数据. 4 | 5 | 比如客户端连续3次调用`send`分别发送了3, 4, 5个字节的数据, 而服务端调用`send(1024)`就会全部接收. 6 | -------------------------------------------------------------------------------- /ghttpd/CHANGES.md: -------------------------------------------------------------------------------- 1 | 1.0.0(2017-05-23) 2 | ------ 3 | 4 | 参考tinyhttpd, 能够正常显示html页面, pythonCGI脚本动态生成响应 -------------------------------------------------------------------------------- /ghttpd/README.md: -------------------------------------------------------------------------------- 1 | # ghttpd 2 | 3 | C语言编写的简单http服务器 4 | 5 | 编译方法 6 | 7 | ``` 8 | $ gcc -g -lpthread ./webserver.c 9 | ``` 10 | 11 | 然后直接运行`a.out`即可. 12 | 13 | 可以这样访问 14 | 15 | ![](./rdsrc/001.png) 16 | 17 | 由python cgi脚本生成如下响应(注意访问参数的存在) 18 | 19 | ![](./rdsrc/002.png) 20 | 21 | POST方法与cgi脚本交互示例 22 | 23 | ![](./rdsrc/003.png) 24 | 25 | 输入用户名密码, 点击'Submit', 可以得到如果结果 26 | 27 | ![](./rdsrc/004.png) 28 | 29 | ------ 30 | 31 | 参考 32 | 33 | [tinyhttpd](https://github.com/AngryHacker/code-with-comments/tree/master/tinyhttpd) 34 | 35 | [如何创建和使用Python CGI脚本](https://linux.cn/article-4377-1.html) 36 | 37 | 这段小程序默认面对GET请求直接返回它所要的文件, 而POST请求则是要执行CGI脚本. 当然, 其实实际场景中GET请求也是可以执行CGI脚本的, 比如前端传入页码参数, 服务端程序就需要查询数据库的指定页数. 38 | 39 | 服务端响应的响应体应该没有结束标志, 当连接关闭就说明响应包已经全部发完了. 40 | 41 | 关于cgi, 应该是某种协议, http服务器按照协议中的格式, 与cgi脚本交互. 这个工程中使用了python的cgi库, 可以获取前端页面传入的表单数据并显示(py文件需要有可执行权限). -------------------------------------------------------------------------------- /ghttpd/htdocs/cgi-bin/form.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!encoding: utf-8 3 | 4 | import cgi 5 | form = cgi.FieldStorage() 6 | print "Content-Type: text/html" 7 | print "" 8 | print "" 9 | print "

CGI Script Output

" 10 | print "

" 11 | print "The user entered data are:
" 12 | print "username: " + form["username"].value + "
" 13 | print "password: " + form["password"].value + "
" 14 | print "

" 15 | print "" 16 | -------------------------------------------------------------------------------- /ghttpd/htdocs/form.html: -------------------------------------------------------------------------------- 1 | 2 |

cgi test 2

3 |
4 | username:
5 | password:
6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /ghttpd/htdocs/index.html: -------------------------------------------------------------------------------- 1 | hello world 2 | hello kugou 3 | hello kitty 4 | -------------------------------------------------------------------------------- /ghttpd/htdocs/index.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!encoding: utf-8 3 | 4 | import cgi 5 | print "Content-Type: text/html" 6 | print "" 7 | print "" 8 | print "

CGI Script Output

" 9 | print "

This page was generated by a Python CGI script.

" 10 | print "" 11 | -------------------------------------------------------------------------------- /ghttpd/rdsrc/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generals-space/networkprogramming/d06198461a194ec99f00c74da55404d0097fb4b4/ghttpd/rdsrc/001.png -------------------------------------------------------------------------------- /ghttpd/rdsrc/002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generals-space/networkprogramming/d06198461a194ec99f00c74da55404d0097fb4b4/ghttpd/rdsrc/002.png -------------------------------------------------------------------------------- /ghttpd/rdsrc/003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generals-space/networkprogramming/d06198461a194ec99f00c74da55404d0097fb4b4/ghttpd/rdsrc/003.png -------------------------------------------------------------------------------- /ghttpd/rdsrc/004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generals-space/networkprogramming/d06198461a194ec99f00c74da55404d0097fb4b4/ghttpd/rdsrc/004.png -------------------------------------------------------------------------------- /ghttpd/webserver.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define SERVER_STR "Server: shttpd/1.0.0\r\n" 16 | #define SERVER_PORT 1234 17 | 18 | void request_handler(int); 19 | int get_line(int, char *, int); 20 | 21 | /* 22 | @Function: 501状态, 不受支持的请求方法. 23 | */ 24 | void status_501(int connect_fd) 25 | { 26 | char buf[1024]; 27 | sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n"); 28 | send(connect_fd, buf, sizeof(buf), 0); 29 | } 30 | 31 | /* 32 | @Function: 404错误, 文件不存在 33 | */ 34 | void status_404(int connect_fd) 35 | { 36 | char buf[1024]; 37 | sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n"); 38 | send(connect_fd, buf, sizeof(buf), 0); 39 | } 40 | 41 | void status_400(int connect_fd) 42 | { 43 | char buf[1024]; 44 | sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n"); 45 | send(connect_fd, buf, sizeof(buf), 0); 46 | } 47 | 48 | void status_500(int connect_fd) 49 | { 50 | char buf[1024]; 51 | sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n"); 52 | send(connect_fd, buf, sizeof(buf), 0); 53 | } 54 | 55 | /* 56 | 从目标socket套接字中获取一行. 57 | http请求的换行符可能有3种: 换行符linefeed(\n), 回车符carriage return(\r), 或是两者的结合(\r\n) 58 | 59 | 如果直接buf装满了也还没发两现换行符则把buf的最后一个字符设置为'\0'; 60 | 如果读到上面3种任意一种换行符, 则buf的结尾为'\n', 结尾后再跟一个'\0'. 61 | */ 62 | int get_line(int connect_fd, char *buf, int size) 63 | { 64 | int i = 0; 65 | char c = '\0'; 66 | int n; 67 | // 如果c为字符'\n', 说明这一行已经读取完毕(当然, 是在buf未满的情况下) 68 | while((i < size -1) && (c != '\n')) 69 | { 70 | n = recv(connect_fd, &c, 1, 0); 71 | if(n <= 0) 72 | { 73 | c = '\n'; 74 | } 75 | else 76 | { 77 | // 如果发现接收到的是字符'\r', 则继续, 因为下一个很可能是'\n', 它们共同组成http协议中的换行 78 | if(c == '\r') 79 | { 80 | // MSG_PEEK标志可以使用下一次recv读取时依然从此次内容开始, 可以理解为窗口不滑动 81 | n = recv(connect_fd, &c, 1, MSG_PEEK); 82 | // 如果的确是换行符就把它吸收到, 窗口前移, 不能影响下一次读取. 83 | if((n > 0) && (c == '\n')) 84 | { 85 | recv(connect_fd, &c, 1, 0); 86 | } 87 | // 如果读到了其他字符, 说明'\r'本身就是一个换行符了, 结束读取 88 | else 89 | { 90 | c = '\n'; 91 | } 92 | } 93 | buf[i] = c; 94 | i ++; 95 | } 96 | } 97 | buf[i] = '\0'; 98 | return i; 99 | } 100 | 101 | 102 | /* 103 | 确切的说这是一个200响应的响应头 104 | */ 105 | void response_header(int connect_fd) 106 | { 107 | char buf[1024]; 108 | strcpy(buf, "HTTP/1.0 200 OK\r\n"); 109 | send(connect_fd, buf, strlen(buf), 0); 110 | strcpy(buf, SERVER_STR); 111 | send(connect_fd, buf, strlen(buf), 0); 112 | strcpy(buf, "Content-type: text/html\r\n"); 113 | send(connect_fd, buf, strlen(buf), 0); 114 | strcpy(buf, "\r\n"); 115 | send(connect_fd, buf, strlen(buf), 0); 116 | } 117 | 118 | void response_file(int connect_fd, FILE *file) 119 | { 120 | char buf[1024]; 121 | int msg_len; 122 | // fgets以换行为分隔符. 123 | fgets(buf, sizeof(buf), file); 124 | while(!feof(file)) 125 | { 126 | msg_len = send(connect_fd, buf, strlen(buf), 0); 127 | // 这一句只有下次http请求到来时才会输出, 应该是下次请求到来前socket不算真正的关闭??? 128 | printf("send %d bytes", msg_len); 129 | fgets(buf, sizeof(buf), file); 130 | } 131 | } 132 | void serve_file(int connect_fd, const char *filename) 133 | { 134 | FILE *file = NULL; 135 | char buf[1024]; 136 | int buf_len = 1; 137 | 138 | buf[0] = 'A'; buf[1] = '\0'; 139 | while(buf_len > 0 && strcmp("\n", buf)) 140 | buf_len = get_line(connect_fd, buf, sizeof(buf)); 141 | 142 | file = fopen(filename, "r"); 143 | if(file == NULL) 144 | status_404(connect_fd); 145 | else 146 | { 147 | response_header(connect_fd); 148 | response_file(connect_fd, file); 149 | } 150 | fclose(file); 151 | } 152 | 153 | void exec_cgi(int connect_fd, const char *filename, const char *method, const char *query_str) 154 | { 155 | char buf[1024]; 156 | int buf_len = 1; 157 | int cgi_input[2]; 158 | int cgi_output[2]; 159 | pid_t pid; 160 | int status; 161 | int i; 162 | char c; 163 | int cnt_len = -1; 164 | 165 | buf[0] = 'A'; buf[1] = '\0'; 166 | if(strcasecmp(method, "GET") == 0) 167 | { 168 | while(buf_len > 0 && strcmp("\n", buf)) 169 | buf_len = get_line(connect_fd, buf, sizeof(buf)); 170 | } 171 | else 172 | { 173 | buf_len = get_line(connect_fd, buf, sizeof(buf)); 174 | while(buf_len > 0 && strcmp("\n", buf)) 175 | { 176 | // buf[15]正好是Content-Length:的冒号之后的位置. 177 | buf[15] = '\0'; 178 | if(strcasecmp(buf, "Content-Length:") == 0) 179 | { 180 | cnt_len = atoi(&buf[16]); 181 | } 182 | buf_len = get_line(connect_fd, buf, sizeof(buf)); 183 | } 184 | // 没有找到content-length字段 185 | if(cnt_len == -1){ 186 | status_400(connect_fd); 187 | return ; 188 | } 189 | } 190 | 191 | sprintf(buf, "HTTP/1.0 200 OK\r\n"); 192 | send(connect_fd, buf, strlen(buf), 0); 193 | 194 | /* 建立管道*/ 195 | if (pipe(cgi_output) < 0) { 196 | /*错误处理*/ 197 | status_500(connect_fd); 198 | return ; 199 | } 200 | /*建立管道*/ 201 | if (pipe(cgi_input) < 0) { 202 | /*错误处理*/ 203 | status_500(connect_fd); 204 | return ; 205 | } 206 | if ((pid = fork()) < 0 ) { 207 | /*错误处理*/ 208 | status_500(connect_fd); 209 | return ; 210 | } 211 | 212 | if(pid == 0) 213 | { 214 | // 由于管道的存在, 子进程中无法通过标准输出打印到控制台任何信息, 215 | // 需要在父进程的read方法中取到. 216 | char env_method[255]; 217 | char env_query[255]; 218 | char env_length[255]; 219 | 220 | dup2(cgi_input[0], 0); 221 | dup2(cgi_output[1], 1); 222 | close(cgi_input[1]); 223 | close(cgi_output[0]); 224 | 225 | // 设置环境变量 226 | sprintf(env_method, "REQUEST_METHOD=%s", method); 227 | putenv(env_method); 228 | 229 | if (strcasecmp(method, "GET") == 0) { 230 | /*设置 query_string 的环境变量*/ 231 | sprintf(env_query, "QUERY_STRING=%s", query_str); 232 | putenv(env_query); 233 | } 234 | else 235 | { 236 | /*设置 content_length 的环境变量*/ 237 | sprintf(env_length, "CONTENT_LENGTH=%d", cnt_len); 238 | putenv(env_length); 239 | } 240 | printf("before exec %s...\n", filename); 241 | //使用execl函数运行cgi 242 | execl(filename, filename, NULL); 243 | exit(0); 244 | } 245 | else 246 | { 247 | close(cgi_input[0]); 248 | close(cgi_output[1]); 249 | // 把POST请求中带的数据, 传递给CGI子进程. 250 | if(strcasecmp(method, "POST") == 0) 251 | { 252 | for(i = 0; i < cnt_len; i ++) 253 | { 254 | recv(connect_fd, &c, 1, 0); 255 | write(cgi_input[1], &c, 1); 256 | } 257 | } 258 | while(read(cgi_output[0], &c, 1) > 0) 259 | { 260 | printf(&c); 261 | send(connect_fd, &c, 1, 0); 262 | } 263 | close(cgi_input[1]); 264 | close(cgi_output[0]); 265 | waitpid(pid, &status, 0); 266 | } 267 | } 268 | 269 | void request_handler(int connect_fd) 270 | { 271 | char buf[1024]; 272 | int buf_len; 273 | char method[255]; 274 | char url[255]; 275 | char path[512]; 276 | int i, j; 277 | struct stat filestat; 278 | int is_cgi = 0; 279 | char *query_str = NULL; 280 | 281 | // 得到http请求的第一行, 一般是: GET / http1.1\r\n这种形式 282 | buf_len = get_line(connect_fd, buf, sizeof(buf)); 283 | printf("The first line in request is %s\n", buf); 284 | 285 | // 从第一行中取出请求方法GET或是POST 286 | i = 0; 287 | while(!isspace(buf[i]) && i < (buf_len - 1)) 288 | { 289 | method[i] = buf[i]; 290 | i ++; 291 | } 292 | method[i] = '\0'; 293 | printf("The method is %s\n", method); 294 | 295 | // strcasecmp()忽略大小写比较字符串 296 | if(strcasecmp(method, "GET") && strcasecmp(method, "POST")) 297 | { 298 | status_501(connect_fd); 299 | return ; 300 | } 301 | 302 | // POST请求的时候开启CGI 303 | if(strcasecmp(method, "POST") == 0) 304 | { 305 | is_cgi = 1; 306 | } 307 | 308 | // 接下来读取请求中的url, 还是在第一行. 309 | while(isspace(buf[i]) && i < (buf_len - 1)) 310 | { 311 | i ++; 312 | } 313 | j = 0; 314 | while(!isspace(buf[i]) && i < (buf_len - 1) && j < (sizeof(url) - 1)) 315 | { 316 | url[j] = buf[i]; 317 | i ++; j ++; 318 | } 319 | url[j] = '\0'; 320 | printf("The url is %s\n", url); 321 | 322 | // 处理得到url中的请求参数, 格式为?a=1&b=2&c=3 323 | if(strcasecmp(method, "GET") == 0) 324 | { 325 | query_str = url; 326 | while(*query_str != '?' && *query_str != '\0') 327 | { 328 | // 当前字符不是?, 也没有到字符串末尾, 则指针后移 329 | query_str ++; 330 | } 331 | // 如果*query_str是'?'(由于上面while的存在, 如果不是'?'就没什么事了...) 332 | if(*query_str == '?') 333 | { 334 | is_cgi = 1; 335 | *query_str = '\0'; 336 | query_str ++; 337 | // 这样舍弃前面的字符, 会不会出问题啊?? 338 | // ...错了, query_str只是一个字符指针, 本身并没有存储数据 339 | // *query_str写入'\0'时, 其实是在url空间中修改的, 这样url只能输出'?'符号前的数据 340 | // 而query_str则可以得到'?'符号后面的数据. 341 | printf("query string is %s\n", query_str); 342 | } 343 | } 344 | 345 | // 组装文件路径, 默认在htdocs目录中寻找 346 | sprintf(path, "htdocs%s", url); 347 | if(path[strlen(path) - 1] == '/') 348 | { 349 | strcat(path, "index.html"); 350 | } 351 | printf("Target path is %s\n", path); 352 | 353 | // 最终路径必须定位到一个文件 354 | if(stat(path, &filestat) == -1) 355 | { 356 | // 如果定位不到目标文件, 说明文件找不到, 请求头的剩余信息也没什么用了, 直接丢弃吧 357 | while(buf_len > 0 && strcmp("\n", buf)) 358 | buf_len = get_line(connect_fd, buf, sizeof(buf)); 359 | status_404(connect_fd); 360 | } 361 | else 362 | { 363 | // tinyhttpd中根据文件是否具有可执行权限判断是否执行cgi, 这是不合理的. 364 | // if((filestat.st_mode & S_IXUSR) || (filestat.st_mode & S_IXGRP) || (filestat.st_mode & S_IXOTH)) 365 | // is_cgi = 1; 366 | 367 | if(!is_cgi){ 368 | serve_file(connect_fd, path); 369 | } 370 | else 371 | { 372 | exec_cgi(connect_fd, path, method, query_str); 373 | } 374 | } 375 | close(connect_fd); 376 | } 377 | 378 | int tcp_server() 379 | { 380 | struct sockaddr_in server_addr; 381 | int listen_fd; 382 | 383 | if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 384 | { 385 | perror("socket create error"); 386 | exit(1); 387 | } 388 | memset(&server_addr, 0, sizeof(server_addr)); 389 | server_addr.sin_family = AF_INET; 390 | server_addr.sin_addr.s_addr = htonl(INADDR_ANY); 391 | server_addr.sin_port = htons(SERVER_PORT); 392 | 393 | if(bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){ 394 | perror("bind error"); 395 | exit(1); 396 | } 397 | 398 | if(listen(listen_fd, 20) < 0) 399 | { 400 | perror("listen error"); 401 | exit(1); 402 | } 403 | printf("accepting connections on port %d\n", SERVER_PORT); 404 | return listen_fd; 405 | } 406 | 407 | int main() 408 | { 409 | struct sockaddr_in client_addr; 410 | socklen_t client_addr_len; 411 | int listen_fd, connect_fd; 412 | pthread_t subthread; 413 | 414 | listen_fd = tcp_server(); 415 | 416 | while(1) 417 | { 418 | client_addr_len = sizeof(client_addr); 419 | connect_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len); 420 | if(connect_fd < 0) 421 | { 422 | perror("accept error"); 423 | exit(1); 424 | } 425 | 426 | if(pthread_create(&subthread, NULL, request_handler, connect_fd) != 0) 427 | { 428 | perror("thread create error"); 429 | } 430 | } 431 | close(listen_fd); 432 | return 0; 433 | } 434 | --------------------------------------------------------------------------------