socket 计算机中的一种网络传输机制,对 TCP 和 UDP 的封装,自动帮我们在底层完成各种协议操作,接收到数据包后返回到上层应用。
socket 分为客户端和服务端,它的工作模型为:
一、 socket 网络地址
1.1 网络字节序
关于字节序的概念可以查看计算机中的字节序。
一般来说,计算机是低字节序,网络传输是高字节序,两者之间并不统一。使用时需要通过以下函数进行转换:
1 2 3 4 5 |
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); |
记忆方式
h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数。
例如端口号是 16 位 (s),主机 (h) 转网络 (n) 字节序的函数为 htons
。
1.2 IP 地址转换函数
socket 地址
计算机中的 IP 地址是一个 32 位长整数,因为 ip 地址最多为 255.255.255.255
,每个点位最多占 1 个字节=8 位,所以 IP 地址为 32 位整数。
我们用的地址是一个 sockaddr
类型,它包含了地址族,端口号和 IP 地址等信息。不过它是很早以前的地址结构了,为了适应需要,现在衍生出了 sockaddr_in
等地址类型如下图所示。
但是为了向前兼容,现在 sockaddr 退化成了 (void *)
的作用,传递一个地址给函数,至于这个函数是 sockaddr_in
还是 sockaddr_in6
,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。类型的定义如下:
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 |
struct sockaddr { sa_family_t sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */ }; struct sockaddr_in { __kernel_sa_family_t sin_family; /* Address family */ __be16 sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ /* Pad to size of `struct sockaddr'. */ unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)]; }; /* Internet address. */ struct in_addr { __be32 s_addr; }; struct sockaddr_in6 { unsigned short int sin6_family; /* AF_INET6 */ __be16 sin6_port; /* Transport layer port # */ __be32 sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ __u32 sin6_scope_id; /* scope id (new in RFC2553) */ }; struct in6_addr { union { __u8 u6_addr8[16]; __be16 u6_addr16[8]; __be32 u6_addr32[4]; } in6_u; #define s6_addr in6_u.u6_addr8 #define s6_addr16 in6_u.u6_addr16 #define s6_addr32 in6_u.u6_addr32 }; #define UNIX_PATH_MAX 108 struct sockaddr_un { __kernel_sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ }; |
一般我们常用的是 sockaddr_in
,简单用法为:
1 2 3 4 |
sockaddr_in clnt_addr; clnt_addr.sin_family = AF_INET; clnt_addr.sin_port = htons(9999); clnt_addr.s_addr.sin_addr = INADDR_ANY; |
地址转换函数
早期 IPv4 地址转换函数:
1 2 3 4 5 6 |
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp); in_addr_t inet_addr(const char *cp); char *inet_ntoa(struct in_addr in); |
在后期引入 IPv6 后的转换函数支持 IPv4 和 IPv6:
1 2 3 4 5 |
#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 size); |
其中 af
是地址族,一般填写 AF_INET
表示以太网。
二、 socket 操作函数
2.1 socket
socket 函数用于创建一个 socket 对象,在 linux 环境中,socket 也是一个文件,因此该函数实际返回的是一个文件描述符。
1 2 3 |
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); |
参数说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
domain: - AF_INET 这是大多数用来产生 socket 的协议,使用 TCP 或 UDP 来传输,用 IPv4 的地址。 - AF_INET6 与上面类似,不过是来用 IPv6 的地址。 - AF_UNIX 本地协议,使用在 Unix 和 Linux 系统上,一般都是当客户端和服务器在同一台及其上的时候使用。 type: - SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的 socket 类型,这个 socket 是使用 TCP 来进行传输。。 - SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用 UDP 来进行它的连接。。 - SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取 - SOCK_RAW 这个 socket 类型提供单一的网络访问,这个 socket 类型使用 ICMP 公共协议。 (ping 、 traceroute 使用该协议) 。 - SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。 protocol: - 0 默认协议 返回值: 成功返回一个新的文件描述符,失败返回-1,设置 errno 。 |
2.2 bind
bind 用于绑定地址到 socket 。
1 2 3 |
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
参数说明
1 2 3 4 |
sockfd: socket 文件描述符。 addr: 构造出 IP 地址加端口号。 addrlen: sizeof(addr)长度。 返回值: 成功返回 0,失败返回-1, 设置 errno 。 |
绑定前要先设置好地址:
1 2 3 4 5 |
struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(8000); |
2.3 listen
listen 用于监听某个端口号:
1 2 3 |
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); |
参数说明
1 2 3 |
sockfd: socket 文件描述符。 backlog: 排队建立 3 次握手队列和刚刚建立 3 次握手队列的链接数和。 返回值: listen()成功返回 0,失败返回-1 。 |
查看系统默认 backlog
1 2 |
> cat /proc/sys/net/ipv4/tcp_max_syn_backlog 128 |
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的 accept() 返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未 accept 的客户端就处于连接等待状态,listen() 声明 sockfd 处于监听状态,并且最多允许有 backlog 个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
2.4 accept
服务端接受一个 socket 连接,此时的连接已经三次握手完成
1 2 3 |
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
参数说明
1 2 3 4 |
**sockdf: socket 文件描述符。 **addr: 传出参数,返回链接客户端地址信息,含 IP 地址和端口号。 **addrlen: 传入传出参数 (值-结果), 传入 sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小。 **返回值: 成功返回一个新的 socket 文件描述符,用于和客户端通信,失败返回-1,设置 errno 。 |
三次握手完成后,服务器调用 accept() 接受连接,如果服务器调用 accept() 时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。 addr 是一个传出参数,accept() 返回时传出客户端的地址和端口号。 addrlen 参数是一个传入传出参数 (value-resultargument),传入的是调用者提供的缓冲区 addr 的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度 (有可能没有占满调用者提供的缓冲区) 。如果给 addr 参数传 NULL,表示不关心客户端的地址。
2.5 connect
客户端连接服务端的函数,此时开始三次握手。
1 2 3 |
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
参数说明:
1 2 3 4 |
sockdf: socket 文件描述符。 addr: 传入参数,指定服务器端地址信息,含 IP 地址和端口号。 addrlen: 传入参数, 传入 sizeof(addr)大小。 返回值: 成功返回 0,失败返回-1,设置 errno 。 |
2.6 recv 和 recvfrom
1 2 3 4 |
#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); |
recv 用于 TCP 协议读取数据:
1 2 3 4 5 |
sockfd: 连接的 socket 文件描述符。 buf: 用来保存读取数据的变量。 len: 读取的数据大小 flags: 读取标志,一般设置为 0 。 返回值: 成功返回读取到的字节数,失败返回-1,0 表示已经断开连接。 |
recv 用于 udp 协议,参数含义类似。
由于 socket 也是一个文件描述符,因此也可以使用 read 来读取 socket 中的数据。
2.7 send 和 sendto
1 2 3 4 5 |
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); |
send 和 sendto 分别用于 tcp 和 udp 协议,参数含义和上面的 recv 类似,也可以使用 write 向 socket 中些数据。
三、一个简单的服务端和客户端案例
以下是一个示例 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 |
// client.c #include<sys/types.h> #include<sys/socket.h> #include<errno.h> #include<stdio.h> #include<string.h> #include<stdlib.h> #include<arpa/inet.h> #include<unistd.h> #include<string.h> const int SERVER_PORT = 8080; const int MAX_BUF_SIZE = 1024; const char HOST[] = "127.0.0.1"; int main(int argc, char** argv){ if (argc < 3){ printf("Usage: ./client host port"); return 0; } int clnt_fd, n; struct sockaddr_in serv_addr; char send_buf[MAX_BUF_SIZE]; clnt_fd = socket(AF_INET, SOCK_STREAM, 0); if (clnt_fd == -1){ perror("socket error"); return 0; } // set server addr bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET, argv[1], &serv_addr.sin_addr); if (connect(clnt_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){ perror("connect error"); return 0; } printf("connect to server success!\n"); int index = 0; while (index<100){ bzero(send_buf, MAX_BUF_SIZE); scanf("%s", send_buf); if(strcmp("exit", send_buf) == 0){ break; } n = write(clnt_fd, send_buf, strlen(send_buf)); if (n == -1){ perror("write error"); return 0; } n = read(clnt_fd, send_buf, n); if (n == -1){ perror("read error"); return 0; } printf("%s\n", send_buf); } close(clnt_fd); return 0; } |
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 |
// server.c #include<sys/types.h> #include<sys/socket.h> #include<arpa/inet.h> #include<errno.h> #include<stdio.h> #include<string.h> #include<unistd.h> #include<ctype.h> #include<stdlib.h> const int SERV_PORT = 8080; const int MAX_CONN = 1; const int MAX_BUF_SIZE = 1024; const int MAX_IP_SIZE = 17; void toUpper(char buf[]){ int i = 0; while (i < MAX_BUF_SIZE && buf[i]){ buf[i] = toupper(buf[i]); i++; } } int main(int argc, char** argv){ if (argc < 2){ printf("./server port\n"); return 0; } int nPort = atoi(argv[1]); int clnt_fd, serv_fd; struct sockaddr_in clnt_addr, serv_addr; socklen_t len = sizeof(serv_addr); char buf[MAX_BUF_SIZE], ip[MAX_IP_SIZE]; int n; serv_fd = socket(AF_INET, SOCK_STREAM, 0); if (serv_fd == -1){ perror("Create socket error"); return 0; } bzero(&serv_addr, sizeof(serv_addr)); bzero(&clnt_addr, sizeof(clnt_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(nPort); if (bind(serv_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){ perror("Bind socket error"); return 0; } if (listen(serv_fd, MAX_CONN) == -1){ perror("Listen error"); return 0; } printf("server hs start and listen %d\n", nPort); clnt_fd = accept(serv_fd, (struct sockaddr*)&serv_addr, &len); if (clnt_fd == -1){ perror("Accpet error"); return 0; } inet_ntop(AF_INET, &serv_addr.sin_addr, ip, sizeof(serv_addr)); printf("Accept[%s:%d]\n", ip, serv_addr.sin_port); bzero(buf, MAX_BUF_SIZE); while((n = read(clnt_fd, buf, MAX_BUF_SIZE)) != 0){ if (n == -1){ perror("Read error"); continue; } printf("Receive %d byte data: %s\n", n, buf); toUpper(buf); write(clnt_fd, buf, n); bzero(buf, MAX_BUF_SIZE); } printf("client[%s:%d] has disconnected!\n", ip, serv_addr.sin_port); close(clnt_fd); return 0; } |
运行结果
服务端通过./server 9988
启动,监听 9988 端口。客户端连接端口输入字符串测试:
1 2 3 4 5 6 7 8 9 |
> ./client 127.0.0.1 9988 connect to server success! helloworld HELLOWORLD client CLIENT maqian MAQIAN exit # 输入 exit 退出 |
服务端显示:
1 2 3 4 5 6 7 |
root@ma:/data/code/c/2-socket/test# ./server 9988 server hs start and listen 9988 Accept[127.0.0.1:50336] Receive 10 byte data: helloworld Receive 6 byte data: client Receive 6 byte data: maqian client[127.0.0.1:50336] has disconnected! |
评论