一、套接字socket
1.1 概念
1> 最早的套接字也是跟消息队列、共享内存、管道一样,只能实现同一主机之间的多个进程间的通信
2> 随着tcp/ip协议族的出现,使得消息能够穿过网卡设备,在网络中进行传输
3> 套接字通信使用的是套接字文件,也是一种特殊的文件,bcd-lsp中的s说的就是该文件
4> socket这个函数,用于创建一个套接字文件,套接字文件的通信原理如下所示
1.2 socket函数介绍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); 功能:创建一个用于通信的端点,并返回该通信对应的文件描述符,描述符使用最小未分配原则 参数1:通信域对应的协议族 AF_UNIX, AF_LOCAL Local communication(本地通信) unix(7) man 7 unix可以查看相信信息 AF_INET IPv4 Internet protocols(IPv4通信) ip(7) man 7 ip可以查看相信信息 AF_INET6 IPv6 Internet protocols(IPv6通信) ipv6(7) man 7 ipv6可以查看相信信息 参数2:指定通信语义,理解成传输方式 SOCK_STREAM 提供支持TCP通信 SOCK_DGRAM 提供支持UDP通信 SOCK_RAW 通过原始的套接字通信 参数3:通信协议,如果参数2指定了确定的通信方式,该参数填0即可 如果不确定通信方式,可用的参数有: TCP:IPPROTO_TCP UDP:IPPROTO_UDP 返回值:成功返回套接字文件描述符,失败返回-1并置位错误码
|
二、基于TCP的基本通信
2.1 通信流程
2.2 服务器端相关API
1> 创建套接字:
该套接字用于接收客户端的连接请求使用,并不是用于通信的套接字
2> bind绑定端口号和IP地址
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
| #include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能:给指定的套接字文件描述符绑定IP地址和端口号 参数1:要绑定的套接字文件描述符 参数2:地址信息结构体,包含了通信域、IP地址、端口号 struct sockaddr { sa_family_t sa_family; char sa_data[14]; } 以上结构体是通用地址信息结构体,无论TCP还是UDP都可以使用,主要用于形参的强制类型转换,避免出现编译报错,但是具体的通信族有不同的地址信息结构体 对于IPv4通信,需要man 7 ip进行查看 struct sockaddr_in { sa_family_t sin_family; 通信域 in_port_t sin_port; 端口号,网络字节序 struct in_addr sin_addr; 网络地址,是一个结构体 }; struct in_addr { uint32_t s_addr; IP地址,网络字节序 }; 对于本地的通信,需要man 7 unix查看 struct sockaddr_un { sa_family_t sun_family; 标识域套接字 char sun_path[108]; 套接字文件的名字 }; 参数3:参数2的大小 返回值: 成功返回0,失败返回-1并置位错误码
|
3> listen设置监听
1 2 3 4 5 6 7 8
| #include <sys/types.h> #include <sys/socket.h> int listen(int sockfd, int backlog); 功能:将套接字文件描述符设置成监听状态,以监听客户端的连接请求 参数1:服务器套接字文件描述符 参数2:挂起队列的长度,当该队列长度满时,其他连接的客户端将会报错,一般设置为128 返回值: 成功返回0,失败返回-1并置位错误码
|
4> accept阻塞等待客户端连接请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 功能:阻塞接收客户端的连接请求,并给该客户端创建一个新的用于通信的套接字 参数1:用于连接的套接字文件描述符 参数2:用于接收客户端的地址信息的结构体指针,如果不愿意接收,填NULL即可 参数3:接收客户端的地址信息的长度,是一个指针,也需要传递变量的地址 返回值:成功返回一个用于通信的套接字文件描述符失败返回-1并置位错误码 注意: 1、bind error: Address already in use:说明端口号已经被占用 2、bind error: Cannot assign requested address:说明IP地址填写错误
|
5> 数据收发函数:send、recv
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
| #include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); 功能:向套接字文件描述符中发送数据 参数1:要发送的套接字文件描述符 参数2:要发送数据的起始地址 参数3:要发送数据的大小 参数4:标识是否阻塞 0:表示阻塞 MSG_DONTWAIT:表示非阻塞 返回值:成功返回发送的字节个数,失败返回-1并置位错误码 #include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); 功能:从给的的套接字文件描述符中读取数据 参数1;要读取的套接字文件描述符 参数2:读取数据的容器地址 参数3:读取的大小 参数4: 标识是否阻塞 0:表示阻塞 MSG_DONTWAIT:表示非阻塞 返回值: >0:表示接收的字符的个数 =0:表示对方已经退出 =-1:出错,置位错误码
|
6> 服务器端实现代码
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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| #include<myhead.h> #define SER_PORT 8888 #define SER_IP "192.168.125.78" int main(int argc, const char *argv[]) { int sfd = socket(AF_INET, SOCK_STREAM, 0); if(sfd == -1) { perror("socket error"); return -1; } printf("sfd = %d\n", sfd); int reuse = 1; if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) { perror("setsockopt error"); return -1; } printf("端口号快速重用成功\n"); struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(SER_PORT); sin.sin_addr.s_addr = inet_addr(SER_IP); if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) == -1) { perror("bind error"); return -1; } printf("bind success %s %s %d\n", __FILE__, __func__, __LINE__); if(listen(sfd, 128) == -1) { perror("listen error"); return -1; } printf("listen success %s %s %d\n", __FILE__, __func__, __LINE__); struct sockaddr_in cin; socklen_t socklen = sizeof(cin); int newfd = -1; newfd = accept(sfd, (struct sockaddr*)&cin, &socklen); if(newfd == -1) { perror("accept error"); return -1; } printf("[%s:%d]发来链接请求 %s %s %d\n", \ inet_ntoa(cin.sin_addr), ntohs(cin.sin_port),__FILE__, __func__, __LINE__); char buf[128] = ""; while(1) { bzero(buf, sizeof(buf)); int res = recv(newfd, buf, sizeof(buf), 0); if(res == 0) { printf("客户端已经下线\n"); break; } printf("[%s:%d] : %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf); strcat(buf, "*_*"); send(newfd, buf, sizeof(buf), 0); printf("发送成功\n"); } close(newfd); close(sfd); return 0; }
|
2.3 客户端端相关API及实现
1> socket:创建一个用于通信的套接字文件描述符
2> bind:可绑定也可以不绑定
3> connect:连接服务器
4> send\recv: 通信
5> close:关闭套接字
1 2 3 4 5 6 7 8 9
| #include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能:将指定的客户端套接字,连接到服务器 参数1;客户端套接字文件描述符 参数2:服务器的地址信息结构体 参数3:参数2的大小 返回值:成功返回0,失败返回-1并置位错误码
|
6> 客户端的实现
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
| #include<myhead.h> #define SER_PORT 8888 #define SER_IP "192.168.125.78" #define CLI_PORT 6666 #define CLI_IP "192.168.125.78" int main(int argc, const char *argv[]) { int cfd = -1; cfd = socket(AF_INET, SOCK_STREAM, 0); if(cfd == -1) { perror("socket error"); return -1; } printf("cfd = %d\n", cfd); struct sockaddr_in cin; cin.sin_family = AF_INET; cin.sin_port = htons(CLI_PORT); cin.sin_addr.s_addr = inet_addr(CLI_IP); if(bind(cfd, (struct sockaddr*)&cin, sizeof(cin)) == -1) { perror("bind error"); return -1; } printf("bind success\n"); struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(SER_PORT); sin.sin_addr.s_addr = inet_addr(SER_IP); if(connect(cfd, (struct sockaddr*)&sin, sizeof(sin)) == -1) { perror("connect error"); return -1; } printf("connect success\n"); char buf[128] = ""; while(1) { bzero(buf, sizeof(buf)); printf("请输入>>>"); fgets(buf, sizeof(buf), stdin); buf[strlen(buf)-1] = 0; send(cfd, buf, sizeof(buf), 0); printf("发送成功\n"); if(strcmp(buf, "quit") == 0) { break; } recv(cfd, buf, sizeof(buf), 0); printf("[%s:%d]:%s\n", SER_IP, SER_PORT, buf); } close(cfd); return 0; }
|
三、基于UDP的基础通信
3.1 通信模型
3.2 UDP服务器模型
1> socket:创建用于通信的套接字文件描述符
2> bind:绑定端口号和IP地址
3> sendto/recvfrom:数据收发
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
| ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); 功能:读取客户端发来的消息 参数1:当前套接字文件描述符 参数2:存放数据的容器起始地址 参数3:读取数据的大小 参数4:是否阻塞 0:表示阻塞 MSG_DONTWAIT:表示非阻塞 参数5:接受客户端地址信息结构体的容器起始地址 参数6:参数5的大小 返回值: >=0:返回读取的字节个数 =-1:失败,置位错误码 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 功能:向指定的套接字文件描述符中发送数据 参数1:当前套接字文件描述符 参数2:要发送数据的容器起始地址 参数3:发送数据的大小 参数4:是否阻塞 0:表示阻塞 MSG_DONTWAIT:表示非阻塞 参数5:接受消息端地址信息结构体的容器起始地址 参数6:参数5的大小 返回值:成功返回发送字节的个数,失败返回-1并置位错误码
|
4> close:关闭客户端
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| #include<myhead.h> #define SER_PORT 9999 #define SER_IP "192.168.125.78" int main(int argc, const char *argv[]) { int sfd = socket(AF_INET, SOCK_DGRAM, 0); if(sfd == -1) { perror("socket error"); return -1; } printf("sfd = %d\n", sfd); struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(SER_PORT); sin.sin_addr.s_addr = inet_addr(SER_IP); if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) == -1) { perror("bind error"); return -1; } char buf[128] = ""; struct sockaddr_in cin; socklen_t socklen = sizeof(cin); while(1) { bzero(buf, sizeof(buf)); recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, &socklen); printf("读取的消息为:%s\n", buf); strcat(buf, "*_*"); if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, sizeof(cin)) == -1) { perror("write error"); return -1; } printf("发送成功\n"); } close(sfd); return 0; }
|
3.3 UDP客户端实现
1> socket 创建套接字
2> bind 可以绑定也可以不绑定
3> 数据收发
4> 关闭套接字
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
| #include<myhead.h> #define SER_PORT 9999 #define SER_IP "192.168.125.104" int main(int argc, const char *argv[]) { int cfd = socket(AF_INET, SOCK_DGRAM, 0); if(cfd == -1) { perror("socket error"); return -1; } struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(SER_PORT); sin.sin_addr.s_addr = inet_addr(SER_IP); char buf[128] = ""; while(1) { printf("请输入>>>"); fgets(buf, sizeof(buf), stdin); buf[strlen(buf)-1] = '\0'; sendto(cfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, sizeof(sin)); if(strcmp(buf, "quit") == 0) { break; } recvfrom(cfd, buf, sizeof(buf), 0, NULL, NULL); printf("收到消息:%s\n", buf); } close(cfd); return 0; }
|
3.4 udp中使用connect函数(笔试面试题)
1> udp中可以使用connect函数,但是不会产生三次握手
2> udp中一般在服务器端使用connect函数。
3> udp中如果使用的connect函数,那么该服务器将与指定的客户端会建立唯一的通道,其他客户端向该服务器发消息,就不再接收了
4> udp中可以多次使用connect函数,连接不同的客户端进行通信,也可以设置都不连接,需要将sin_family成员设置成AF_UNSPCE再次进行连接函数
5> 当服务器跟某个客户端连接后,就可以像TCP一样完成使用read/write、send/recv函数了
6> UDP中使用connect的好处
提高传输效率:
不连接时:将信息填充到内核 —> 发送数据 —>清空内核数据 —> 将信息填充到内核 —> 发送数据 —>清空内核数据
连接后:将信息填充到内核 —> 发送数据 —> 发送数据 —> 发送数据 —> 发送数据 —> 发送数据
提高数据传输的稳定性:
当服务器连接了A客户端后,建立了唯一的通道,其他客户端再发消息就不接受了,不会影响到A客户端与服务器的沟通
如果不连接,可能会存在A客户端向服务器发送消息过程中,B客户端也向服务器发消息
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
| #include<myhead.h> #define SER_PORT 9999 #define SER_IP "192.168.125.104" int main(int argc, const char *argv[]) { int sfd = socket(AF_INET, SOCK_DGRAM, 0); if(sfd == -1) { perror("socket error"); return -1; } printf("sfd = %d\n", sfd); struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(SER_PORT); sin.sin_addr.s_addr = inet_addr(SER_IP); if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) == -1) { perror("bind error"); return -1; } char buf[128] = ""; struct sockaddr_in cin; socklen_t socklen = sizeof(cin); while(1) { bzero(buf, sizeof(buf)); recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, &socklen); if(strcmp(buf,"connect")==0) { connect(sfd, (struct sockaddr*)&cin, sizeof(cin)); send(sfd, buf, sizeof(buf), 0); }else if(strcmp(buf,"disconnect")==0) { cin.sin_family = AF_UNSPEC; connect(sfd, (struct sockaddr*)&cin, sizeof(cin)); } printf("读取的消息为:%s\n", buf); strcat(buf, "*_*"); if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, sizeof(cin)) == -1) { perror("write error"); return -1; } printf("发送成功\n"); } close(sfd); return 0; }
|
本章完