1、什么是套接字
Socket是封装了TCP/IP协议簇的系统API接口,这使得程序员无需关注协议本身,直接使用socket提供的接口与不同主机间的进程互联通信。
目前市面上主流的操作系统都采用这套机制进制网络通信,所以不同种类的操作系统,使用不同的编程语言,只要调用操作系统提供的Socket接口,都能进行网络通信。
2、基于TCP协议的Socket编程模型
被连接者需要完成的任务(服务端): 1、创建socket内核对象,内核创建完成后会返回它的描述符(该描述只是为了完成连接,三次握手) 2、准备本机地址(ip地址+端口号) 3、绑定(把本机地址与socket对象进行绑定) 4、开启监听,并设置排队的队列长度 5、等待连接,连接成功后,内核会再返回一个连接成功的Socket描述符,专门用来通信 for(;;) { 6、接收请求 7、返回结果 } 8、关闭通信的Socket对象 9、关闭连接的Socket对象 连接者需要完成的任务(客户端): 1、创建socket内核对象,内核创建完成后会返回它的描述符 2、准备被连接者的地址(ip地址+端口号) 3、发起连接,使用Socket+地址(ip地址+端口号)发起连接请求 for(;;) { 4、发送请求 5、接收结果 } 6、关闭Socket对象
3、TCP通信需要使用Socket接口
int socket(int domain, int type, int protocol); 功能:创建Socket对象 domain: AF_UNIX, AF_LOCAL 采用本地socket文件进行通信,如果用它则只能本机上的两个进程进行通信 AF_INET IPv4地址 AF_INET6 IPv6地址 type: SOCK_STREAM 数据流 TCP SOCK_DGRAM 报文 UDP protocol: 特殊通信协议,写0即可 返回值: 成功则返回Socket对象描述符,失败返回-1。 // 基本地址类型,它是socket系列接口的表面参数,而实际使用的是sockaddr_un或sockaddr_in,我们需要把sockaddr_in强制转换成sockaddr类型。 struct sockaddr_in { sa_family_t sin_family; // 地址类型,与domain保持一致即可 in_port_t sin_port; // 端口号,网络字节序的2字节整数 struct in_addr sin_addr.s_addr; // IP地址,网络字节序的4字节整数 }; int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 功能:绑定Socket对象与通信地址 sockfd:Socket对象描述符 addr:通信地址,实际提供可能是sockaddr_un或sockaddr_in,需要对它们进行强制转换 addrlen:addr结构体的字节数 返回值:成功返回0,失败返回-1 int listen(int sockfd, int backlog); 功能:开启Socket对象的监听 sockfd:Socket地址描述符 backlog:备胎的数量 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 功能:等待连接,没有成功连接之前,会进入阻塞状态 sockfd:Socket对象描述符 addr:用于存储连接者的通信地址 addrlen: 既是输入(告诉accetp接口,addr结构体的字节数),也是输出(实际接收到的addr结构的字节数) 返回值:建立连接的,能够通信的Socket对象描述符,失败返回-1 int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 功能:让sockfd对象向addr地址发起连接 sockfd:Socket对象描述符 addr:连接目标的地址 addrlen:addr结构体的字节数 返回值:成功返回0,失败返回-1 ssize_t recv(int sockfd, void *buf, size_t len, int flags); 功能:从Socket对象读接收干字节 sockfd:Socket对象描述符 buf:接收数据的内存块首地址 len:buf的字节数 flags:是否阻塞,写0即可 返回值:成功接收到了多少个字节,失败返回-1 ssize_t send(int sockfd, const void *buf, size_t len, int flags); 功能:通过Socket对象发送若干字节 sockfd:Socket对象描述符 buf:要发送的内存块首地址 len:要发送的字节数 flags:是否阻塞,写0即可 返回值:成功发送了多少个字节,失败返回-1 int close(int fd); 功能:关闭fd描述所代表的内核对象 uint16_t htons(uint16_t hostshort); 功能:把本地字节序的 unsigned short 类型的数据转换网络字节序 in_addr_t inet_addr(const char *cp); 功能:把字符串格式 点分十进制的ip地址 转换成网络字节序的4字节ip地址
练习1:实现基于TCP的回声服务器(收到到什么消息就返回什么消息),服务端能一对多通信。
问题1:服务器如何能服务多个客户端?
问题2:服务器如何限制客户端数量(最大在线人数)?
问题3:如何让服务端的线程循环使用,而不要频繁的创建、销毁。
#include <stdio.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <semaphore.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> // 定义信号量 sem_t sem; void* run(void* arg) { // 备份sockfd int sockfd = *(int*)arg; char buf[BUFSIZ]; for(;;) { // 接收数据 int ret = read(sockfd,buf,BUFSIZ); if(0 >= ret || !strcmp("quit",buf)) break; printf("recv:%s byte:%d\n",buf,ret); strcat(buf,",return!"); // 返回数据 ret = write(sockfd,buf,strlen(buf)+1); if(0 >= ret) break; } printf("通信结束!\n"); // 关闭负责通信的socket对象 close(sockfd); // 信号量加1 sem_post(&sem); } int main(int argc,const char* argv[]) { // 创建socket对象 int server_fd = socket(AF_INET,SOCK_STREAM,0); if(0 > server_fd) { perror("socket"); return -1; } // 准备本机地址 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(2233); addr.sin_addr.s_addr = inet_addr("10.0.2.15"); socklen_t addrlen = sizeof(addr); // 绑定socket对象和地址 if(bind(server_fd,(struct sockaddr*)&addr,addrlen)) { perror("bind"); return -2; } // 开启监听 if(listen(server_fd,6)) { perror("listen"); return -3; } // 初始化信号量 sem_init(&sem,0,5); // 循环的等待连接 for(;;) { // 信号量减1 sem_wait(&sem); int sockfd = accept(server_fd,(struct sockaddr*)&addr,&addrlen); if(0 > sockfd) { perror("accept"); break; } // 创建子线程为客户端服务 pthread_t tid; pthread_create(&tid,NULL,run,&sockfd); // 防止客户端同时连接,导致子线程来不及备份sockfd usleep(100); // 分离子线程 pthread_detach(tid); } // 关闭负责连接的socket对象 close(server_fd); // 关闭主线程,不能让它影响子线程为客户端服务 pthread_exit(NULL); return 0; }
#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main(int argc,const char* argv[]) { // 创建socket对象 int sockfd = socket(AF_INET,SOCK_STREAM,0); if(0 > sockfd) { perror("socket"); return -1; } // 准备服务端的地址 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(2233); addr.sin_addr.s_addr = inet_addr("10.0.2.15"); socklen_t addrlen = sizeof(addr); // 连接 if(connect(sockfd,(struct sockaddr*)&addr,addrlen)) { perror("connect"); return -2; } char buf[BUFSIZ]; for(;;) { printf(">>>"); scanf("%s",buf); // 发送数据 int ret = write(sockfd,buf,strlen(buf)+1); if(0 >= ret || !strcmp("quit",buf)) break; // 接收数据 ret = read(sockfd,buf,BUFSIZ); if(0 >= ret) break; printf("recv:%s byte:%d\n",buf,ret); } printf("通信结束!\n"); // 关闭socket对象 close(sockfd); return 0; }
4、基于UDP协议的Socket编程模型
被接收者需要完成的任务(服务端): 1、创建socket内核对象,内核创建完成后会返回它的描述符 type:SOCK_DGRAM 2、准备本机地址(ip地址+端口号) 3、绑定(把本机地址与socket对象进行绑定) for(;;) { 4、接收请求,同时接收发送者的地址 5、返回结果,按发送者的地址进行返回 } 6、关闭Socket对象 发送者需要完成的任务: 1、创建socket内核对象,内核创建完成后会返回它的描述符 type:SOCK_DGRAM 2、准备接收者的地址(ip地址+端口号) for(;;) { 3、发送请求,根据接收者的地址发送数据 4、接收结果,并接收返回者的地址 } 5、关闭Socket对象
5、UDP通信需要使用Socket接口
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 功能:UDP专用的数据发送函数。 dest_addr:收件人的地址。 addrlen:地址长度。 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); 功能:UDP专用的数据接收函数。 src_addr:发件人的地址,也是数据返回时的地址 addrlen:地址长度,既是输入也是输出
练习:参考TCP的回声服务器,实现基于UDP协议的回声服务器。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main(int argc,const char* argv[]) { int sockfd = socket(AF_INET,SOCK_DGRAM,0); if(0 > sockfd) { perror("socket"); return -1; } struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(3344); addr.sin_addr.s_addr = inet_addr("10.0.2.15"); socklen_t addrlen = sizeof(addr); if(bind(sockfd,(struct sockaddr*)&addr,addrlen)) { perror("bind"); return -2; } char buf[BUFSIZ]; for(;;) { int ret = recvfrom(sockfd,buf,BUFSIZ,0, (struct sockaddr*)&addr,&addrlen); if(0 >= ret) break; printf("recv:%s byte:%d from:%s:%hu\n", buf,ret,inet_ntoa(addr.sin_addr),ntohs(addr.sin_port)); strcat(buf,",return!"); ret = sendto(sockfd,buf,strlen(buf)+1,0, (struct sockaddr*)&addr,addrlen); if(0 >= ret) break; } printf("通信结束!\n"); close(sockfd); return 0; }
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main(int argc,const char* argv[]) { int sockfd = socket(AF_INET,SOCK_DGRAM,0); if(0 > sockfd) { perror("socket"); return -1; } struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(3344); addr.sin_addr.s_addr = inet_addr("10.0.2.15"); socklen_t addrlen = sizeof(addr); if(connect(sockfd,(struct sockaddr*)&addr,addrlen)) { perror("connect"); return -2; } char buf[BUFSIZ]; for(;;) { printf(">>>"); scanf("%s",buf); //int ret = sendto(sockfd,buf,strlen(buf)+1,0, // (struct sockaddr*)&addr,addrlen); int ret = send(sockfd,buf,strlen(buf)+1,0); if(0 >= ret || !strcmp("quit",buf)) break; // ret = recvfrom(sockfd,buf,BUFSIZ,0, // (struct sockaddr*)&addr,&addrlen); ret = recv(sockfd,buf,BUFSIZ,0); if(0 >= ret) break; printf("recv:%s byte:%d from:%s:%hu\n",buf,ret,inet_ntoa(addr.sin_addr),ntohs(addr.sin_port)); } printf("通信结束!\n"); close(sockfd); return 0; }
6、UDP的连接操作
UPD协议底层是否需要连接操作,客户端但可以在Socket层面进行连接,连接后的Socket对象在后续的通信过程中就不再需要通信地址了。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> int main(int argc,const char* argv[]) { if(3 != argc) { puts("Use:./cmd <ip> <port>"); return 0; } int sockfd = socket(AF_INET,SOCK_DGRAM,0); if(0 > sockfd) { perror("socket:"); return -1; } struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(atoi(argv[2])); addr.sin_addr.s_addr = inet_addr(argv[1]); socklen_t addrlen = sizeof(addr); // UDP协议的sockfd对象连接后,在通信时就不需要提供地址了 if(connect(sockfd,(struct sockaddr*)&addr,addrlen)) { perror("connect"); return -2; } char buf[BUFSIZ]; for(;;) { printf(">>"); scanf("%s",buf); int ret = send(sockfd,buf,strlen(buf)+1,0); if(0 > ret) break; ret = recv(sockfd,buf,BUFSIZ,0); if(0 > ret) break; printf("recv:%s byte:%d\n",buf,ret); } close(sockfd); }
7、本地Socket通信
网络Socket通信是把网卡抽象成Socket文件配合TCP/IP协议簇,能够使当前进程与其它计算机的进程进行网络通信。
本地Socket通信是在文件系统中创建Socket文件,能够使当前进程与本机的其它进程进行通信(IPC进程间通信)。
使用sockaddr_un类型的通信地址,当调用socket对象与通信地址绑定时,会自动创建socket文件。
// 本地地址类型 #include <sys/un.h> struct sockaddr_un { sa_family_t sun_family; // AF_UNIX, AF_LOCAL char sun_path[]; // 套接字文件路径 }; #include <sys/socket.h> int bind (int sockfd, const struct sockaddr* addr,socklen_t addrlen);
8、基于TCP协议的本地Socket通信
#include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <sys/un.h> int main(int argc,const char* argv[]) { int sockfd = socket(AF_LOCAL,SOCK_STREAM,0); if(0 > sockfd) { perror("socket"); return -1; } struct sockaddr_un addr = {}; addr.sun_family = AF_LOCAL; strcpy(addr.sun_path,"tcp_socket"); socklen_t addrlen = sizeof(addr); if(bind(sockfd,(struct sockaddr*)&addr,addrlen)) { perror("bind"); return -1; } if(listen(sockfd,3)) { perror("listen"); return -1; } for(;;) { int clifd = accept(sockfd,NULL,0); if(0 > clifd) { perror("accept"); sleep(1); continue; } if(fork()) continue; char buf[4096]; size_t buf_size = sizeof(buf); for(;;) { printf("recv...\n"); int ret = recv(clifd,buf,buf_size,0); if(0 >= ret || 0 == strcmp("quit",buf)) { printf("结束通信!"); break; } buf[ret] = '\0'; printf("recv:%s byte:%d\n",buf,ret); strcat(buf,":return"); ret = send(clifd,buf,strlen(buf)+1,0); if(0 >= ret) { printf("结束通信!"); break; } } close(clifd); return 0; } return 0; }
#include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <sys/un.h> int main(int argc,const char* argv[]) { int sockfd = socket(AF_LOCAL,SOCK_STREAM,0); if(0 > sockfd) { perror("sockfd"); return -1; } struct sockaddr_un addr = {}; addr.sun_family = AF_LOCAL; strcpy(addr.sun_path,"tcp_socket"); socklen_t addrlen = sizeof(addr); if(connect(sockfd,(struct sockaddr*)&addr,addrlen)) { perror("connect"); return -1; } char buf[4096]; size_t buf_size = sizeof(buf); for(;;) { printf(">>>"); scanf("%s",buf); int ret = send(sockfd,buf,strlen(buf)+1,0); if(0 >= ret || 0 == strcmp("quit",buf)) { printf("通信结束!\n"); break; } ret = recv(sockfd,buf,buf_size,0); if(0 >= ret) { printf("通信结束!\n"); break; } printf("recv:%s byte:%d\n",buf,ret); } close(sockfd); return 0; }
任务:参考TCP的本地Socket地址,实现基于UDP的本地Socket通信
#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <sys/un.h> int main(int argc,const char* argv[]) { // 创建socket对象 int sockfd = socket(AF_LOCAL,SOCK_DGRAM,0); if(0 > sockfd) { perror("socket"); return -1; } // 准备通信地址(本机的) struct sockaddr_un addr = {}, src_addr = {}; addr.sun_family = AF_LOCAL; strcpy(addr.sun_path,"udp_ctos"); socklen_t addrlen = sizeof(addr); // 绑定地址与socket对象 if(bind(sockfd,(struct sockaddr*)&addr,addrlen)) { perror("bind"); return -1; } char buf[4096]; size_t buf_size = sizeof(buf); for(;;) { // 接收数据 int size = recvfrom(sockfd,buf,buf_size,0,(struct sockaddr*)&src_addr,&addrlen); printf("from:%s recv:%s byte:%d\n",src_addr.sun_path,buf,size); strcat(buf,":return"); // 返回数据 sendto(sockfd,buf,strlen(buf)+1,0,(struct sockaddr*)&src_addr,addrlen); } return 0; }
#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <sys/un.h> int main(int argc,const char* argv[]) { // 创建socket对象 int sockfd = socket(AF_LOCAL,SOCK_DGRAM,0); if(0 > sockfd) { perror("socket"); return -1; } // 准备通信地址(自己的) struct sockaddr_un addr = {}, dest_addr = {}; addr.sun_family = AF_LOCAL; strcpy(addr.sun_path,"udp_stoc"); socklen_t addrlen = sizeof(addr); // 绑定地址与socket对象 if(bind(sockfd,(struct sockaddr*)&addr,addrlen)) { perror("bind"); return -1; } // 目标的 dest_addr.sun_family = AF_LOCAL; strcpy(dest_addr.sun_path,"udp_ctos"); char buf[4096]; size_t buf_size = sizeof(buf); for(;;) { printf(">>>"); scanf("%s",buf); if(0 == strcmp("quit",buf)) { printf("通信结束!"); close(sockfd); break; } sendto(sockfd,buf,strlen(buf)+1,0,(struct sockaddr*)&dest_addr,addrlen); int size = recvfrom(sockfd,buf,buf_size,0,(struct sockaddr*)&dest_addr,&addrlen); printf("recv:%s byte:%d\n",buf,size); } }