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! |
评论