5.1.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
| #include <stdio.h>
void byteOrder(){ union { short value; char union_bytes[sizeof(short)]; }test; test.value = 0x0102; if(test.union_bytes[0] == 1 && test.union_bytes[1] == 2){ printf("大端字节序\n"); } else if(test.union_bytes[0] == 2 && test.union_bytes[1] == 1){ printf("小端字节序\n"); } else{ printf("不清楚\n"); } }
int main(){ byteOrder(); return 0; }
|
5.1.3专用socket
地址
TCP/IP
协议族有sockaddr_in
和sockaddr_in6
两个专用socket
地址结构体,他们分别用于IPv4
和IPv6
,这里我只介绍sockaddr_in
。
1 2 3 4 5 6 7 8 9
| struct sockaddr_in{ sa_family_t sin_family; u_int16_t sin_port; struct in_addr sin_addr; }
struct in_addr{ u_int32_t s_addr; }
|
注意:所有专用socket
地址类型的变量在实际使用时都需要转换为通用socket
地址类型sockaddr
(强制转换即可),因为所有socket
编程接口使用的地址参数的类型都是sockaddr
。
5.1.4IP
地址转换函数
1 2 3 4 5 6 7 8 9 10 11
| #include <arpa/inet.h> int inet_pton(int af, const char *src, void *dst); const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
#include <netinet/in.h> #define INET_ADDRSTRLEN 16 #define INET6_ADDRSTRLEN 64
|
5.2创建socket
1 2 3 4 5 6 7 8 9
| #include <sys/types.h> #include <sys/socket.h>
int socket(int domain, int type, int protocol)
|
socket系统调用成功时返回一个socket文件描述符,失败返回-1并设置errno
5.3命名socket
创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体socket地址。将一个socket与socket地址绑定称为给socket命名。在服务器程序中,我们通常需要命名socket,因为只有命名后客户端才知道该如何连接它。客户端通常不需要命名socket,而是采用匿名方式,也就是使用操作系统自动分配的socket地址。
1 2 3 4 5 6 7 8 9
| #include <sys/types.h> #include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
|
5.4监听socket
socket被命名之后,还不能立即接收客户端连接,我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接
1 2 3 4 5 6 7
| #include <sys/socket.h> int listen(int sockfd, int backlog);
|
下面我们编写一个程序测试一下
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
| #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <assert.h> #include <stdio.h> #include <string.h>
static bool stop = false;
static void term_handler(int sig){ stop = true; }
int main(int argc, char *argv[]){ signal(SIGTERM, term_handler);
if(argc <= 3){ printf("usage: %s ip_address port_num backlog\n", basename(argv[0])); return 1; }
char *ip = argv[1]; int port = atoi(argv[2]);
int sockfd = socket(PF_INET, SOCK_STREAM, 0); assert(sockfd != -1);
int sign = 0; struct sockaddr_in server_addr; server_addr.sin_family = PF_INET; server_addr.sin_port = htons(port); inet_pton(PF_INET, ip, &server_addr.sin_addr);
sign = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); assert(sign != -1);
sign = listen(sockfd, 5); assert(sign != -1);
while(!stop){ sleep(1); }
close(sockfd); return 0; }
|
这个命令组合使用了两个命令:netstat
和
grep
,并通过管道(|
)将第一个命令的输出作为第二个命令的输入。我会为你逐步解释它:
netstat -nt
:
netstat
:
这是一个命令行工具,用于显示网络状态,包括网络连接、路由表、接口统计等。
-n
:
表示以数字形式显示地址和端口号,而不是尝试解析它们的名称。
因此,netstat -nt
的输出会列出系统上所有活动的TCP连接,同时显示它们的源和目标IP地址以及端口号,并直接显示数字而不进行名称解析。
|
:
- 这是一个管道操作符,用于将前一个命令的输出作为后一个命令的输入。
grep 8000
:
grep
:
是一个强大的文本搜索工具,用于搜索匹配的字符串。
8000
: 是你想在 netstat
的输出中搜索的字符串。
这个命令会从 netstat
的输出中筛选出所有包含 “8000”
的行,这通常意味着你正在查找与端口 8000
相关的所有活动连接。
综上所述,netstat -nt | grep 8000
会显示所有在端口
8000
上的活动TCP连接。
5.5接受连接
代码:接受一个异常的连接
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
| #include <iostream> #include <stdio.h> #include <libgen.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <assert.h>
int main(int argc, char* argv[]){ if(argc <= 2){ printf("运行程序,需输入这三个参数:%s, ip_address, port_num\n", basename(argv[0])); return 1; }
const char* ip = argv[1]; int port = atoi(argv[2]);
struct sockaddr_in address; address.sin_family = AF_INET; inet_pton(AF_INET, ip, &address.sin_addr); address.sin_port = ntohs(port);
int sock = socket(AF_INET, SOCK_STREAM, 0); assert(sock >= 0);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address)); assert(ret != -1);
ret = listen(sock, 5); assert(ret != -1);
sleep(20);
struct sockaddr_in client; socklen_t client_len = sizeof(client); int connfd = accept(sock, (struct sockaddr*)&client, &client_len); if(connfd < 0){ printf("errno is: %d\n", errno); }else{ char remote[INET_ADDRSTRLEN]; printf("connected with ip: %s and port: %d\n", inet_ntop(AF_INET, &client.sin_addr, remote, client_len), ntohs(client.sin_port)); close(connfd); } close(sock); return 0; }
|
1、第一次运行报错,undefined reference to main
,这种情况一般有三种可能:
- 没有定义main函数
- main函数的main拼写错误
- 刚写的代码忘记保存了
2、accept
函数是阻塞的,上述代码即服务器端运行的时候,会阻塞在accept
处,一旦客户端请求建立连接,服务器立马终止程序。注意accept
只是从listen
监听队列中取出连接,它不会理会客户端处于什么状态。
3、一直在思考select/poll/epoll
这些有什么用。首先因为listen是有监听队列的,劣势就在于只能一个个处理,并且同时接入的连接数有限。比如队列长度为5,处理完一个,再建立下一个连接,这样如果某一个连接处理很长时间一直阻塞在那里,就导致后面的新请求连接建立超时。很直观的想法是fork
新进程或者创建新线程来处理新连接,每来一个连接我就创建一个来跟他对接。这样资源消耗太大。因此就有了select/poll/epoll
,先把连接建立起来并放进文件描述符,最后从这里面寻找哪些发生了可读可写事件,也避免了因为读写事件造成的阻塞(没有数据到来就阻塞了)。