Linux学习:socket通信基础
1.socket是什么
所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。
socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的 API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连的进程通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket 通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段信息。
socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。
套接字通信分为两部分:
服务器端:被动接受连接,一般不会主动发起连接
客户端:主动向服务器端发起连接
2.字节序
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。
大端字节序是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。
小端序:高位存高地址
大端序:高位存低地址
字节序转换函数
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释。解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)
网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。
#include <arpa/inet.h> // 转换端口(16位) uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序 uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序 // 转IP(32位) uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序 uint32_t ntohl(uint32_t netlong); // 网络字节序 - 主机字节序
3.socket地址
socket地址是一个结构体,封装端口号和IP等信息
客户端--->服务器(IP,Port)
#include <netinet/in.h> struct sockaddr_in { sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */ in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */ /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -sizeof (in_port_t) - sizeof (struct in_addr)]; //用来对齐内存 }; struct in_addr { in_addr_t s_addr; }; struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port; /* Transport layer port # */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* IPv6 scope-id */ }; typedef unsigned short uint16_t; typedef unsigned int uint32_t; typedef uint16_t in_port_t; typedef uint32_t in_addr_t;
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称 domain)和对应的地址族如下所示:
宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。
不同的协议族的地址值具有不同的含义和长度
注意:所有的专用socket地址(以及socket_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有的socket编程接口使用的地址参数类型都是sockaddr
4.IP地址转换
通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。下面 的函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换
#include <arpa/inet.h> // p:点分十进制的IP字符串,n:表示network,网络字节序的整数 int inet_pton(int af, const char *src, void *dst); af:地址族: AF_INET AF_INET6 src:需要转换的点分十进制的IP字符串 dst:转换后的结果保存在这个里面 // 将网络字节序的整数,转换成点分十进制的IP地址字符串 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); af:地址族: AF_INET AF_INET6 src: 要转换的ip的整数的地址 dst: 转换成IP地址字符串保存的地方 size:第三个参数的大小(数组的大小) 返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
5.TCP通信流程
UDP:面向无连接,可以单播、多播、广播,面向数据报,不可靠
TCP:面向连接的,可靠的,基于字节流,仅支持单播传播
注意:UDP的首部开销是8个字节;TCP的首部开销最少是20字节
对服务器端:
1.创建一个用于监听的套接字
-监听:监听有客户端的连接
-套接字:套接字也是一种文件类型。在Unix/Linux操作系统中,一切皆文件,包括套接字。可以使用类似于文件操作的系统调用来对套接字进行读写操作。在程序中,可以使用文件描述符来引用套接字,就像引用其他类型的文件一样。
2.将这个监听的文件描述符和本地的IP和端口绑定
-客户端连接服务器时就是使用这个套接字(IP+端口号)
3.设置监听,监听的fd开始工作
4.阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,返回得到一个和客户端通信的套接字(fd)
5.通信
- 接收数据
- 发送数据
6.通信结束,断开连接
对客户端:
1.创建一个用于通信的套接字(fd)
2.连接服务器,需要指定连接的服务器的IP和端口
3.连接成功了,客户端可以直接和服务器通信
4.通信结束,断开连接
6.套接字函数
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略 int socket(int domain, int type, int protocol); - 功能:创建一个套接字 - 参数: - domain: 协议族 AF_INET : ipv4 AF_INET6 : ipv6 AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信) - type: 通信过程中使用的协议类型 SOCK_STREAM : 流式协议 SOCK_DGRAM : 报式协议 - protocol : 具体的一个协议。一般写0 - SOCK_STREAM : 流式协议默认使用 TCP - SOCK_DGRAM : 报式协议默认使用 UDP - 返回值: - 成功:返回文件描述符,操作的就是内核缓冲区。 - 失败:-1 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名 - 功能:绑定,将fd 和本地的IP + 端口进行绑定 - 参数: - sockfd : 通过socket函数得到的文件描述符 - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息 - addrlen : 第二个参数结构体占的内存大小 int listen(int sockfd, int backlog); - 功能:监听这个socket上的连接 - 参数: - sockfd : 通过socket()函数得到的文件描述符 - backlog : 未连接的和已经连接的和的最大值,正整数 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); - 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 - 参数: - sockfd : 用于监听的文件描述符 - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port) - addrlen : 指定第二个参数的对应的内存大小 - 返回值: - 成功 :用于通信的文件描述符 - -1 : 失败 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); - 功能: 客户端连接服务器 - 参数: - sockfd : 用于通信的文件描述符 - addr : 客户端要连接的服务器的地址信息 - addrlen : 第二个参数的内存大小 - 返回值:成功 0, 失败 -1 ssize_t write(int fd, const void *buf, size_t count); // 写数据 ssize_t read(int fd, void *buf, size_t count); // 读数据
7.端口复用
防止服务器重启时之前绑定的端口还未释放(无法连接到重启之前建立连接的客户端)
程序突然退出而系统没有释放端口
#include <sys/types.h> #include <sys/socket.h> int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); 参数: level:选项所在的协议层,常用的有SOL_SOCKET,IPPROTO_TCP,IPPROTO_IP等 optname:选项名称,常用的有SO_REUSEADDR,SO_REUSEPORT,SO_KEEPALIVE等 optval:指向选项值的指针 optlen:选项值的长度,通常使用sizeof函数计算 //示例代码,端口复用的时机是在服务器绑定端口之前 //见书p89页表格,SOL_SOCKET协议层下的SO_REUSEPORT选项对应的选项值的类型是int型,定义一个int = 1表示开启 int optval = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
8.TCP通信的简单实现
服务器端:
// TCP 通信的服务器端 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main() { // 1.创建socket(用于监听的套接字) int lfd = socket(AF_INET, SOCK_STREAM, 0); if(lfd == -1) { perror("socket"); exit(-1); } // 2.绑定 struct sockaddr_in saddr; saddr.sin_family = AF_INET; //使用INADDR_ANY 表示套接字将接受来自任何可用IP地址的连接请求或数据包。 //这种方式通常用于服务器程序,因为服务器需要监听来自任何可用IP地址的连接请求,以便能够接受来自任何客户端的连接。 saddr.sin_addr.s_addr = INADDR_ANY; //使用9999端口 saddr.sin_port = htons(9999); int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr)); if(ret == -1) { perror("bind"); exit(-1); } // 3.监听 ret = listen(lfd, 8); if(ret == -1) { perror("listen"); exit(-1); } // 4.接收客户端连接,clientaddr是传出参数,accept返回用于通信的套接字 struct sockaddr_in clientaddr; int len = sizeof(clientaddr); int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len); if(cfd == -1) { perror("accept"); exit(-1); } // 输出客户端的信息 char clientIP[16]; inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP)); unsigned short clientPort = ntohs(clientaddr.sin_port); printf("client ip is %s, port is %d\n", clientIP, clientPort); // 5.通信 char recvBuf[1024] = {0}; while(1) { // 获取客户端的数据 int num = read(cfd, recvBuf, sizeof(recvBuf)); if(num == -1) { perror("read"); exit(-1); } else if(num > 0) { printf("recv client data : %s\n", recvBuf); } else if(num == 0) { // 表示客户端断开连接 printf("clinet closed..."); break; } char * data = "hello,i am server"; // 给客户端发送数据 write(cfd, data, strlen(data)); } // 关闭文件描述符 close(cfd); close(lfd); return 0; }
客户端:
// TCP通信的客户端 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main() { // 1.创建套接字 int fd = socket(AF_INET, SOCK_STREAM, 0); if(fd == -1) { perror("socket"); exit(-1); } // 2.连接服务器端 struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr); serveraddr.sin_port = htons(9999); int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); if(ret == -1) { perror("connect"); exit(-1); } // 3. 通信 char recvBuf[1024] = {0}; while(1) { char * data = "hello,i am client"; // 给客户端发送数据 write(fd, data , strlen(data)); sleep(1); int len = read(fd, recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); } else if(len > 0) { printf("recv server data : %s\n", recvBuf); } else if(len == 0) { // 表示服务器端断开连接 printf("server closed..."); break; } } // 关闭连接 close(fd); return 0; }
9.使用多进程实现的客户端
#include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <wait.h> #include <errno.h> void recyleChild(int arg) { while(1) { int ret = waitpid(-1, NULL, WNOHANG);//-1表示等待任意子进程;WNOHANG:非阻塞模式,如果没有子进程状态发生改变,则立即返回0,而不是阻塞等待。 if(ret == -1) { // 发生错误,检查errno break; }else if(ret == 0) { // 这意味着它没有等待到任何子进程的状态改变,并且没有收到任何错误。这通常表示没有正在运行的子进程,或者所有子进程都已经被处理完毕。 break; } else if(ret > 0){ // 被回收了 printf("子进程 %d 被回收了\n", ret); } } } int main() { struct sigaction act; act.sa_flags = 0; sigemptyset(&act.sa_mask);//掩码清空 act.sa_handler = recyleChild; // 注册信号捕捉 sigaction(SIGCHLD, &act, NULL); // 创建socket int lfd = socket(AF_INET, SOCK_STREAM, 0); if(lfd == -1){ perror("socket"); exit(-1); } struct sockaddr_in saddr; saddr.sin_family = AF_INET; saddr.sin_port = htons(9999); saddr.sin_addr.s_addr = INADDR_ANY; // 绑定服务器端的IP和端口 int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr)); if(ret == -1) { perror("bind"); exit(-1); } // 监听 ret = listen(lfd, 128); if(ret == -1) { perror("listen"); exit(-1); } // 不断循环等待客户端连接 while(1) { struct sockaddr_in cliaddr; int len = sizeof(cliaddr); // 接受连接 int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len); //如果在循环等待时收到一个SIGCHD,accept的调用可能会被中断,下一次循环无法进入accept。返回-1并设置EINTR if(cfd == -1) { if(errno == EINTR) { continue; } perror("accept"); exit(-1); } // 每一个连接进来,创建一个子进程跟客户端通信 pid_t pid = fork(); if(pid == 0) { // 子进程 // 获取客户端的信息 char cliIp[16]; inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp)); unsigned short cliPort = ntohs(cliaddr.sin_port); printf("client ip is : %s, prot is %d\n", cliIp, cliPort); // 接收客户端发来的数据 char recvBuf[1024]; while(1) { int len = read(cfd, &recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); }else if(len > 0) { printf("recv client : %s\n", recvBuf); } else if(len == 0) { printf("client closed....\n"); break; } write(cfd, recvBuf, strlen(recvBuf) + 1); } close(cfd); exit(0); // 退出当前子进程,发送SIGCHD信号 } } close(lfd); return 0; }
10.使用多线程实现的客户端
每次有客户端连接进来时,创建一个子线程去和它进行通信
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
从线程创建函数中我们可以看出,子线程的工作函数start_routine只有一个参数,我们在工作函数中需要使用连接进来的客户端的文件描述符fd,客户端的IP地址addr以及有可能用到子线程的线程id,一个参数无法传入这么多的数据
因此最好的方式是创建一个结构体类型sockInfo来存储这三个信息,将这个结构体作为工作函数的参数传入
#include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> struct sockInfo { int fd; // 通信的文件描述符 struct sockaddr_in addr; pthread_t tid; // 线程号 }; // 客户端的信息被打包放在sockInfo结构体中 struct sockInfo sockinfos[128];//最多创建128个子线程进行通信 void * working(void * arg) { // 子线程和客户端通信 cfd 客户端的信息 线程号 // 获取客户端的信息 struct sockInfo * pinfo = (struct sockInfo *)arg; char cliIp[16]; inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp)); unsigned short cliPort = ntohs(pinfo->addr.sin_port); printf("client ip is : %s, prot is %d\n", cliIp, cliPort); // 接收客户端发来的数据 char recvBuf[1024]; while(1) { int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); }else if(len > 0) { printf("recv client : %s\n", recvBuf); } else if(len == 0) { printf("client closed....\n"); break; } write(pinfo->fd, recvBuf, strlen(recvBuf) + 1); } close(pinfo->fd); return NULL; } int main() { // 创建socket int lfd = socket(PF_INET, SOCK_STREAM, 0); if(lfd == -1){ perror("socket"); exit(-1); } struct sockaddr_in saddr; saddr.sin_family = AF_INET; saddr.sin_port = htons(9999); saddr.sin_addr.s_addr = INADDR_ANY; // 绑定 int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr)); if(ret == -1) { perror("bind"); exit(-1); } // 监听 ret = listen(lfd, 128); if(ret == -1) { perror("listen"); exit(-1); } // 初始化数据,清空sockinfos中的每一个sockinfo的文件描述符和线程id int max = sizeof(sockinfos) / sizeof(sockinfos[0]); for(int i = 0; i < max; i++) { bzero(&sockinfos[i], sizeof(sockinfos[i])); sockinfos[i].fd = -1; sockinfos[i].tid = -1; } // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信 while(1) { struct sockaddr_in cliaddr; int len = sizeof(cliaddr); // 接受连接 int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len); struct sockInfo * pinfo; for(int i = 0; i < max; i++) { // 从这个数组中找到一个可以用的sockInfo元素 if(sockinfos[i].fd == -1) { pinfo = &sockinfos[i]; break; } if(i == max - 1) { sleep(1); i--; } } pinfo->fd = cfd; memcpy(&pinfo->addr, &cliaddr, len); // 创建子线程,第一个参数是传入参数,将创建好的tid保存于其中,第二个参数是线程属性,第三个参数是线程的工作函数void* working (void* arg) // 第四个参数是工作函数的参数 pthread_create(&pinfo->tid, NULL, working, pinfo); // 设置线程分离 pthread_detach(pinfo->tid); } close(lfd); return 0; }