Linux - 网络编程Socket
目录1.特征2.编程步骤详细代码以服务端为例3.关键函数(重要)练习1服务端收图片客户端发图片二、TCP 传输控制协议1.特征2.TCP编程步骤核心机制三次握手四次挥手关键函数除了connect以外都以服务端代码为例练习1双击聊天输入#quit后退出练习2查字典服务端查与发送单词意思客户端发送要查的单词三、总结通信套接字和监听套接字TCP和UDP的区别表格编程步骤差异前言 网络通信/网络编程简单介绍https://blog.csdn.net/m0_75172867/article/details/159252834?spm1001.2014.3001.5501UDP、TCP同属于TCP/IP协议族中的传输层协议。一、UDP用户数据报协议1.特征无连接直接发送、不可靠不保证送达不会重新传、网络延迟低、开销小、有读阻塞、无写阻塞、半双工。常应用于视频流、语音通话、DNS、游戏等。2.编程步骤详细代码以服务端为例创建套接字程序向内核提出创建一个基于内存的套接字描述符socket(AF_INET, SOCK_DGRAM, 0)// 1.创建UDP套接字文件描述符 int udp_fd socket(AF_INET, SOCK_DGRAM, 0); //互联网传输层(udp)默认 if (udp_fd -1) { perror(socket); return 1;绑定地址bind// 2.给套接字(fd)绑定ip端口号 (服务器必须绑定客户端可选) struct sockaddr_in ser, cli; //服务器地址结构体客户端地址结构体 bzero(ser, sizeof(ser)); //清空服务器地址结构体 bzero(cli, sizeof(cli)); //清空客户端地址结构体 ser.sin_family AF_INET; //协议族IPv4 ser.sin_port htons(50000); //端口号: 50000 ser.sin_addr.s_addr inet_addr(192.168.21.129); //绑定本机Linux ip int ret bind(udp_fd, (SA)ser, sizeof(ser)); if (ret -1) { perror(bind); return 1; } socklen_t len sizeof(cli); //客户端地址结构体长度recvfrom/sendto接收数据recvfromwhile (1) //死循环持续监听客户端请求 { char buf[1024]; /*3.接收客户端发来的数据*/ //套接字: (SA)cli: 存储发送数据的客户端地址; len: 客户端地址长度(入参出参) recvfrom(udp_fd, buf, sizeof(buf), 0, (SA)cli, len); /*4. 处理数据拼接当前时间戳*/ time_t tm; //时间类型变量 time(tm); //获取当前系统时间 sprintf(buf, %s %s, buf, ctime(tm)); //把客户端数据时间字符串写入buf发送数据sendto/*5.发送处理后的数据回客户端(SA)cli: 存储发送数据的客户端地址*/ sendto(udp_fd, buf, strlen(buf), 0, (SA)cli, len);关闭套接字close3.关键函数(重要)以下函数返回值在失败时都为-1。1int socket(int domain, int type, int protocol)参数domain 地址族PF_INET AF_INETipv4---互联网程序PF_UNIX AF_UNIX ---单机程序type 套接字类型SOCK_STREAM 流式套接字 》TCPSOCK_DGRAM用户数据报套接字UDPSOCK_RAW 原始套接字 》IP2int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)参数1所在的描述符中取出并从参数2所在的接口设备上发送出去。注意如果是客户端则该函数可以省略由默认接口发送数据。3ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)参数sockfd 本地的套接字idbuff 本地的数据存储一般是要发送的数据。len 要发送的数据长度flags 要发送数据方式0 表示阻塞发送。UDP 发送数据后不需要对方确认这里的阻塞是指的是缓冲区dest_addr: 必选表示要发送到的目标主机信息结构体。addrlen 目标地址长度。返回值成功 发送的数据长度4ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)总结服务端总共四步第一步创建套接字第二步bind绑定IP第三步接收数据第四步传输数据必须先接收才能传输客户端总共五步第一步创建套接字第二步传输数据第三步接收数据第三步关闭文件描述符必须先传输才能接收练习1服务端收图片客户端发图片服务端// server 服务端 #include arpa/inet.h #include bits/sockaddr.h #include fcntl.h #include netinet/in.h #include netinet/ip.h #include stdio.h #include stdlib.h #include string.h #include strings.h #include sys/socket.h #include sys/types.h /* See NOTES */ #include time.h #include unistd.h typedef struct sockaddr *(SA); int main(int argc, char **argv) { // 1 internet udp 默认协议 int udpfd socket(AF_INET, SOCK_DGRAM, 0); //创建一个网络通信端点套接字 //返回一个文件描述符类似文件句柄后续操作都通过这个描述符进行 if (-1 udpfd) { perror(socket); return -1; } struct sockaddr_in ser, cli; // ipv4结构体 bzero(ser, sizeof(ser)); //将内存区域前n个字节设置为0 bzero(cli, sizeof(cli)); ser.sin_port htons(50000); // host to net_short 小端转大端 //函数将主机字节序小端转换为网络字节序大端。 //因为网络协议规定使用大端字节序 ser.sin_addr.s_addr inet_addr(192.168.31.164); //将点分十进制IP字符串转换为网络字节序的32位整数。 //如果字符串无效返回 INADDR_NONE通常为-1 ser.sin_family AF_INET; int ret bind(udpfd, (SA)ser, sizeof(ser)); //绑定将套接字 udpfd 与本地地址 serIP和端口。 //对于服务器必须绑定一个固定端口以便客户端知道往哪里发送数据 if (-1 ret) { perror(bind); return 1; } socklen_t len sizeof(cli); int fd_png open(rev.png, O_WRONLY | O_CREAT, 0666); //创建存照片的文件 //先接收 //循环写入 while (1) { char buf[4096] {0}; int ret recvfrom(udpfd, buf, sizeof(buf), 0, (SA)cli, len); //接收图片 if (0 ret) //传输结束 { break; } write(fd_png, buf, ret); //写入数据 } char *tmp 传输完毕!\n; sendto(udpfd, tmp, strlen(tmp), 0, (SA)cli, len); //发送 return 0; }客户端// cliet 客户端 #include arpa/inet.h #include bits/sockaddr.h #include fcntl.h #include netinet/in.h #include netinet/ip.h #include stdio.h #include stdlib.h #include string.h #include strings.h #include sys/socket.h #include sys/types.h /* See NOTES */ #include time.h #include unistd.h typedef struct sockaddr *(SA); int main(int argc, char **argv) { // 1创建套接字 int udpfd socket(AF_INET, SOCK_DGRAM, 0); if (-1 udpfd) { perror(socket); return -1; } // 2和服务端一样,不需要用bind,以及不定义cli struct sockaddr_in ser; bzero(ser, sizeof(ser)); //配置结构体 ser.sin_family AF_INET;//使用ipv4协议 ser.sin_port htons(50000);//端口要一致 ser.sin_addr.s_addr inet_addr(192.168.31.164);//服务器ip int fd_png open(linux.png, O_RDONLY); //打开图片 while (1) { char buf[4096] {0}; int ret read(fd_png, buf, sizeof(buf)); sendto(udpfd, buf, ret, 0, (SA)ser, sizeof(ser)); printf(正在发送....\n); if (0 ret) { printf(发送完成....\n); break; } } // sendto(udpfd, buf, ret, 0, (SA)ser, sizeof(ser)); char str[100] {0}; recvfrom(udpfd, str, sizeof(str), 0, NULL, NULL); //接收服务器的消息 printf(%s\n, str); return 0; }二、TCP 传输控制协议1.特征1有连接在正式通信之前需要先确定一条链路通信过程中需保持一致。2有应答可靠传输确认、重传、排序3全双工同一时刻同时收发。4有读写阻塞、流式套接字。*流式套接字1 .发送的次数和接收的次不需要对应2 .数据与数据是连续。3. 顺序(tcp), udp不保证顺序到达4.有读阻塞写阻塞64K2.TCP编程步骤核心机制三次握手建立连接的过程。确保双方都能正常收发数据并协商好初始序列号四次挥手断开连接的过程。由于TCP 连接是全双工的关闭需要双方各自“挂电话”。黏包问题发送方多次发送数据但接收方无法正常解析数据的问题。需通过设置结束标志、发送固定大小、自定义协议等方式解决。三次握手四次挥手图中1~3 为三次握手步骤如下第一次握手SYN发起连接客户端 → 服务器SYN同步序列号客户端随机生成一个初始序列号比如1000发送给服务器。表示“想和你建立连接我的初始序号是 1000。”第二次握手SYN ACK确认并回应服务器 → 客户端SYN, ACK服务器收到 SYN 后也随机生成自己的初始序列号比如8000并确认客户端的序列号ACK 1000 1 1001。表示“我收到了你的请求我的初始序号是 8000我也准备好连接了。”第三次握手ACK最终确认客户端 → 服务器ACK客户端收到服务器的 SYNACK 后确认服务器的序列号ACK 8000 1 8001。表示“我收到你的回应了现在连接正式建立”图中7~10 为四次挥手步骤如下第一次挥手FIN客户端发起关闭客户端 → 服务器发送FIN包Finish含义“我的数据发完了想关闭我这一边的连接。”但此时客户端还能收数据第二次挥手ACK服务器确认收到关闭请求服务器 → 客户端发送ACK包含义“收到你的关闭请求但我这边可能还有数据没发完请等一下。”第三次挥手FIN服务器也发起关闭服务器 → 客户端发送FIN包含义“我这边数据也发完了我也要关闭连接了。”第四次挥手ACK客户端最后确认客户端 → 服务器发送ACK包含义“收到那我们都关闭吧。”注意客户端发完这个 ACK 后会进入TIME_WAIT状态等待 2 倍报文最大生存时间防止最后一个 ACK 丢失导致服务器一直重发 FIN。创建监听套接字socket(AF_INET, SOCK_STREAM, 0)绑定地址bind(listfd,(SA)ser,sizeof(ser))监听连接listen(listfd,3)接受连接accept产生用于通信的新套接字读写数据recv/send关闭套接字close(conn)关键函数除了connect以外都以服务端代码为例1int listen(int sockfd, int backlog)/*1.打开网络设备获得文件描述(套接字)*/ //监听套接字 int listfd socket(AF_INET, SOCK_STREAM, 0); // 互联网TCP默认 if (listfd -1) { perror(socket); return 1; }2int bind(int sockfd, struct sockaddr *addr, socklen_t *addrlen)struct sockaddr_in ser, cli; //清空结构体bzero bzero(ser, sizeof(ser)); bzero(cli, sizeof(cli)); ser.sin_family AF_INET; ser.sin_port htons(50000); ser.sin_addr.s_addr INADDR_ANY; /*2.给套接字绑定 ipport方便客户端找到服务器*/ int ret bind(listfd, (SA)ser, sizeof(ser)); /*(SA)ser为什么要转换 bind() 函数的第二个参数要求是 struct sockaddr * 类型通用地址结构体 但实际编程中我们用 struct sockaddr_inIPv4专用—— 它和 struct sockaddr 内存布局兼容 只是字段更易操作因此需要强制转换让编译器认可。*/3int listen(int sockfd, struct sockaddr *addr, socklen_t *addrlen)/*3.进入监听状态*/ //参数2是进行三次握手的排队数 listen(listfd, 3);4int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)/*4.建立连接 触发三次握手*/ int conn accept(listfd, (SA)cli, len); while (1) { char buf[1024];5int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)这是在客户端方使用的函数struct sockaddr_in ser; bzero(ser, sizeof(ser)); ser.sin_family AF_INET; ser.sin_port htons(50000); ser.sin_addr.s_addr inet_addr(192.168.31.60); //服务器ip /*2.主动连接服务器*/ int ret connect(sockfd, (SA)ser, sizeof(ser)); if (ret -1) { perror(connect); return 1; } int i 30; while (i--) { char buf[512] hello,this is tcp test;6ssize_t recv(int sockfd, void *buf, size_t len, int flags)/*5.接收数据*/ // recv返回值0为实际收到的字节数0为对方断开-1为报错 int rd_ret recv(conn, buf, sizeof(buf), 0); if (rd_ret 0) { break; } /*5.5 处理数据*/ printf(cli:%s\n, buf); time_t tm; time(tm); sprintf(buf, %s %s, buf, ctime(tm));7ssize_t send(int sockfd, const void *msg, size_t len, int flags)参数sockfd如果是服务器则是accept的返回值新fd如果是客户端则是sockfd的返回值旧fd/*6.发送数据*/ send(conn, buf, strlen(buf), 0);练习1双击聊天输入#quit后退出服务端// server 服务端 #include arpa/inet.h #include bits/sockaddr.h #include netinet/in.h #include netinet/ip.h #include stdio.h #include stdlib.h #include string.h #include strings.h #include sys/socket.h #include sys/types.h /* See NOTES */ #include sys/wait.h #include time.h #include unistd.h typedef struct sockaddr *(SA); int main(int argc, char **argv) { // 1 internet udp 默认协议 int tcpfd socket(AF_INET, SOCK_STREAM, 0); //创建一个网络通信端点套接字 //返回一个文件描述符类似文件句柄后续操作都通过这个描述符进行 if (-1 tcpfd) { perror(socket); return -1; } struct sockaddr_in ser, cli; // ipv4结构体 bzero(ser, sizeof(ser)); //将内存区域前n个字节设置为0 bzero(cli, sizeof(cli)); ser.sin_port htons(50000); // host to net_short 小端转大端 ser.sin_addr.s_addr INADDR_ANY; ser.sin_family AF_INET; int ret bind(tcpfd, (SA)ser, sizeof(ser)); if (-1 ret) { perror(bind); return 1; } listen(tcpfd, 3); socklen_t len sizeof(cli); int conn accept(tcpfd, (SA)cli, len); if (-1 conn) { perror(conn); return -1; } pid_t pid fork(); //多进程 if (pid 0) //父进程负责收 { while (1) { char buf[1024] {0}; int ret recv(conn, buf, sizeof(buf), 0); //接收 if (strcmp(buf, #quit) 0) { break; } printf(form cli:%s\n, buf); } } else if (pid 0) //子进程负责发 { while (1) { char buf[1024] {0}; fgets(buf, sizeof(buf), stdin); buf[strlen(buf) - 1] \0; send(conn, buf, strlen(buf), 0); //发送 if (strcmp(buf, #quit) 0) { break; } } } else // fork失败 { perror(fork); return -1; } kill(pid, SIGKILL); close(tcpfd); close(conn); return 0; }客户端// cliet 客户端 #include arpa/inet.h #include bits/sockaddr.h #include netinet/in.h #include netinet/ip.h #include stdio.h #include stdlib.h #include string.h #include strings.h #include sys/socket.h #include sys/types.h /* See NOTES */ #include sys/wait.h #include time.h #include unistd.h typedef struct sockaddr *(SA); int main(int argc, char **argv) { // 1 internet udp 默认协议 int tcpfd socket(AF_INET, SOCK_STREAM, 0); //创建一个网络通信端点套接字 //返回一个文件描述符类似文件句柄后续操作都通过这个描述符进行 if (-1 tcpfd) { perror(socket); return -1; } struct sockaddr_in ser; // ipv4结构体 bzero(ser, sizeof(ser)); ser.sin_port htons(50000); // host to net_short 小端转大端 ser.sin_addr.s_addr inet_addr(192.168.31.164); ser.sin_family AF_INET; socklen_t len sizeof(ser); int ret_con connect(tcpfd, (SA)ser, len); // connect函数返回值不返回套接字 if (-1 ret_con) { perror(connect); return -1; } pid_t pid fork(); //多进程 if (pid 0) //父进程负责收 { while (1) { char buf[1024] {0}; int ret recv(tcpfd, buf, sizeof(buf), 0); //接收 printf(form ser :%s\n, buf); } } else if (pid 0) //子进程负责发 { while (1) { char buf[1024] {0}; fgets(buf, sizeof(buf), stdin); buf[strlen(buf) - 1] \0; send(tcpfd, buf, strlen(buf), 0); if (strcmp(buf, #quit) 0) { break; } } } else // fork失败 { perror(fork); return -1; } kill(pid, SIGKILL); close(tcpfd); return 0; }练习2查字典服务端查与发送单词意思客户端发送要查的单词服务端#include fcntl.h #include netinet/in.h #include netinet/ip.h #include stdio.h #include stdlib.h #include string.h #include sys/socket.h #include sys/types.h /* See NOTES */ #include time.h #include unistd.h #define MAX 19662 //实际只有19661个单词,但我的计数是从1开始的 typedef struct sockaddr *(SA); int main(int argc, char **argv) { // 1 打开网络设备获得文件描述(套接字) // 监听套接字 int listfd socket(AF_INET, SOCK_STREAM, 0); if (-1 listfd) { perror(socket); return 1; } struct sockaddr_in ser, cli; //清空 结构体 bzero(ser, sizeof(ser)); bzero(cli, sizeof(cli)); ser.sin_family AF_INET; ser.sin_port htons(50000); ser.sin_addr.s_addr INADDR_ANY; // 2 给套接字绑定 ip port 方便客户端找到服务器 int ret bind(listfd, (SA)ser, sizeof(ser)); if (-1 ret) { perror(bind); return 1; } // 3 进入监听状态 (套接字进入可以被链接的状态) // 参数2 进行三次握手的排队数 listen(listfd, 3); socklen_t len sizeof(cli); // 4 建立链接 (触发三次握手) // 通信套接字表示从服务端来看conn 是客户端的套接字 int conn accept(listfd, (SA)cli, len); if (-1 conn) { perror(accept); return 1; } //接收数据查字典 int fd_dict open(dict.txt, O_RDONLY); //打开字典 FILE *fp fdopen(fd_dict, r); //字典流指针用于后续fgets函数 if (fd_dict 0) { perror(open dict); return -1; } //查找 while (1) { //接收要查找单词 char word[20] {0}; recv(conn, word, sizeof(word), 0); if (strcmp(word, #quit) 0) { close(listfd); close(conn); close(fd_dict); fclose(fp); exit(1); } int num 0; //计数 while (1) //单次查找 { char buf[1024] {0}; //缓冲 char *tmp[2] {0}; //装找到的单词 //判断是否查完单词了 if (num MAX) { char ret[30] {0}; sprintf(ret, 没有找到这个单词%s, word); send(conn, ret, strlen(ret), 0); // printf(没有找到这个单词%s\n, txt); break; } num; //没查到就计数 //查字典,处理数据发送客户端 if (fgets(buf, sizeof(buf), fp) NULL) { lseek(fd_dict, 0, SEEK_SET); continue; //重新到循环第一行开始读取 } //重新找,直到num大于MAX tmp[0] strtok(buf, ); tmp[1] strtok(NULL, \n); if (strcmp(tmp[0], word) 0) //找到了 { char ret[2048] {0}; sprintf(ret, %s的意思是:%s, tmp[0], tmp[1]); send(conn, ret, strlen(ret), 0); //发送给客户端 // printf(%s意思是:%s\n, tmp[0], tmp[1]); break; } } } // 7 断开链接 close(listfd); close(conn); close(fd_dict); fclose(fp); return 0; }客户端#include arpa/inet.h #include fcntl.h #include netinet/in.h #include netinet/ip.h #include stdio.h #include stdlib.h #include string.h #include sys/socket.h #include sys/types.h /* See NOTES */ #include time.h #include unistd.h typedef struct sockaddr *(SA); int main(int argc, char **argv) { // 1 创建套接字 int sockfd socket(AF_INET, SOCK_STREAM, 0); if (-1 sockfd) { perror(socket); return 1; } struct sockaddr_in ser; //清空 结构体 bzero(ser, sizeof(ser)); ser.sin_family AF_INET; ser.sin_port htons(50000); ser.sin_addr.s_addr inet_addr(127.0.0.1); // 2 主动连接服务器 int ret connect(sockfd, (SA)ser, sizeof(ser)); if (-1 ret) { perror(connect); return 1; } int fd open(linux.png, O_RDONLY); if (-1 fd) { perror(open); return 1; } while (1) { char buf[1024] {0}; //文件读取到达结尾 ret read(fd, buf, sizeof(buf)); if (ret 0) { break; } // 3. 发送数据 返回值 0 实际发送的字节数 0 网络状态不好没有发出数据 -1错误 send(sockfd, buf, ret, 0); bzero(buf, sizeof(buf)); // 4. 接收数据 0 实际接收的字节数 0 表示断开 -1 错误 int rd_ret recv(sockfd, buf, sizeof(buf), 0); if (rd_ret 0) { break; } printf(%s\n, buf); } // 5.断开连接 close(sockfd); close(fd); return 0; }三、总结1.UDP和TCP建立连接的过程要熟记熟悉每个函数通信套接字和监听套接字监听套接字 - 服务器启动时创建的“接待员”这是一个被bind()到特定 IP 和端口并通过listen()开启监听的套接字。它就像一个公司的前台电话专门负责“接受新访客的连接请求”。通信套接字 - 为每个客户端新建的“专属客服”当客户端调用connect()连接服务器时服务器通过accept()函数“接起电话”并创建一个全新的套接字专门用于与该客户端的后续通信。TCP和UDP的区别表格编程步骤差异流式套接字TCP服务端socket() → bind() → listen() → accept() → recv()/send() → close()客户端socket() → connect() → send()/recv() → close()数据报套接字UDP服务端socket() → bind() → recvfrom()/sendto() → close()客户端socket() → sendto()/recvfrom() → close()