Linux系统编程(七)网络编程TCP、UDP

本文目录

  • 一、基础知识点
    • 1. IP地址
    • 2. 端口
    • 3. 域名
    • 4. 网络协议类型
    • 5. IP协议类型
    • 6. 字节序
    • 7. socket套接字
  • 二、TCP 常用API
    • 1. socket套接字描述符
    • 2. bind套接字绑定
    • 3. listen设置最大排队数
    • 4. accept接收客户端请求
    • 5. connect连接服务端
    • 6. read读取数据
    • 7. write发送数据
  • 三、UDP 常用API
    • 1. socket套接字描述符
    • 2. bind套接字绑定
    • 3. recvfrom接收数据
    • 4. sendto发送数据
  • 三、编程流程
    • 1.TCP编程
      • 问题
      • 优化
      • ●TCP服务端作为中转站
    • 2.UDP编程

  
在学习本章之前,可以查看 《计算机网络基础》这篇文章进行学习。

一、基础知识点

1. IP地址

   IP 地址的作用是标识计算机的网卡地址,每一台计算机都有一个 IP 地址。在程序中是通过IP 地址来访问一台计算机的。 本节将讲述 IP 地址的一些知识。 IP 地址是用来标识全球计算机地址的一种符号,就比如一个手机的号码,使用这个地址可以访问一个计算机。
   IP 地址具有统一的格式。 IP 地址是 32 位长度的二进制数值, 存储空间是 4 个字节。例如: 11000000 10101000 00000001 00000110 是一台计算机的 IP 地址,但二进制的数值是不便于记忆的,可以把每个字节用一个十进制的整数来表示,既 192.168.1.6
   在同一个网络中, IP 地址是唯一的。因为需要根据 IP 地址来访问一台计算机,所以在可以访问的范围以内,每一台计算机的 IP 地址是唯一的。在终端中输入命令 ifconfig 可以查看本机 IP 信息。

2. 端口

   在网络通信中,IP地址帮助我们定位到特定的计算机,而端口号则帮助我们在这台计算机上找到具体的应用程序或服务。每个应用程序监听不同的端口,以便接收和处理来自网络的请求。端口号通常以数字形式表示,并与IP地址一起使用,以标识网络通信的不同端点。
   通俗举例:在同一个主机上,IP地址是固定的,但是主机上有很多程序功能,这些功能有不同的端口号。我们为了访问主机的某个功能程序,就必须指定其功能对应的端口号。例如主机的IP为:192.168.12.4,QQ的端口号为10,微信的端口号为13。我们为了使用微信这个功能,我们就需要使用:192.168.12.4: 13来使用其功能。

3. 域名

   域名是互联网中用于标识一个网站或服务器的友好名称,它是IP地址的可读形式。域名系统(DNS)将域名转换为IP地址,使用户能够通过简单易记的名字访问网站,而不需要记住难记的IP地址。例如百度的域名为www.baidu.com,我们可以使用这个域名来代替百度的IP地址。小知识:可以使用 ping 命令来查看一个域名所对应的 IP 地址,例:ping www.baidu.com,我们就可以查看其真实的ip地址。

4. 网络协议类型

(1)TCP协议:提供可靠的、面向连接的数据传输,确保数据按顺序传递、不丢失、不重复,并进行错误检测和纠正。常用于需要可靠数据传输的应用,如Web、电子邮件和文件传输。
   TCP 是面向连接的协议。所谓连接,就是两个对等实体为进行数据通信而进行的一种结合。面向连接服务是在数据交换之前,必须先建立连接。当数据交换结束后,则应终止这个连接。面向连接服务具有:连接建立、数据传输和连接释放这三个阶段。在传送数据时是按序传送的。建立链接:三次握手(这个过程我们并不看得到)。
在这里插入图片描述

(2)UDP协议:提供不可靠的、面向无连接的数据传输,数据包可能会丢失、重复或乱序,不进行错误检测和纠正。传输速度比TCP快!常用于实时性要求高、数据量小、丢失少不影响的应用,如视频流、音频流和游戏通信。

在这里插入图片描述

5. IP协议类型

  IPv4和IPv6是两种不同版本的互联网协议(IP),用于标识网络设备的地址并进行数据包路由。它们的主要区别在于地址格式和容量。

(1)IPV4:是第四版互联网协议,目前仍是最广泛使用的IP协议。

●它使用32位地址空间,格式如下:
  地址格式:四个十进制数,每个数范围从0到255,由点分隔。例如:192.168.1.1。
  地址数量:IPv4的地址空间约有42亿个唯一地址(2^32)。
●IPv4的特点
  地址空间有限:由于地址空间较小,IPv4地址逐渐耗尽,尤其随着互联网设备数量的爆炸性增长。
  地址分配:IPv4地址的分配方式包括公共地址和私有地址,私有地址用于局域网内通信,不可在互联网中直接使用(如192.168.x.x)。
  NAT(网络地址转换):由于地址空间有限,NAT技术被广泛应用,使多个设备可以共享一个公共IP地址访问互联网。

(2)IPV6:是第六版互联网协议,设计为IPv4的继任者,提供更大的地址空间和其他改进。

● 它使用128位地址空间,格式如下:
   地址格式:八组十六进制数,每组四个十六进制数字,由冒号分隔。例如:2001:0db8:85a3:0000:0000:8a2e:0370:7334
  地址数量:IPv6的地址空间极其庞大,有约3.4×1038个地址(2128)。
●IPv6的特点
  几乎无限的地址空间:IPv6提供了极其庞大的地址空间,足以满足未来互联网设备的需求。
  简化的地址配置:支持无状态地址自动配置(SLAAC),设备可以自动生成IPv6地址,不需要手动配置或DHCP。
  内置安全性:IPv6支持IPSec协议,提供更好的安全性。
  无NAT需求:由于地址空间充足,不再需要NAT,大大简化了网络结构和提高了传输效率。
  改进的路由和网络配置:IPv6改进了路由选择和地址分配机制,简化了网络配置和管理。

6. 字节序

顾名思义就是字节的存放顺序,就是字节数大于1的数据在内存中的存放顺序,字节数为1的数据就没有顺序的问题。
(1)主机字节序
  数据在主机(计算机)内部的字节顺序。在计算机系统中,数据存储在内存中,字节的存储顺序取决于计算机的体系结构。
  ●有两种主要的字节序:大端序小端序。在大端序中,高位字节存储在低地址,低位字节存储在高地址。在小端序中,低位字节存储在低地址,高位字节存储在高地址。X_86系统一般都是小端序。

(2)网络字节序
  是一种特定的字节序,用于在网络中传输数据。在网络通信中,数据在传输过程中需要保持一致的字节序,以确保不同主机之间能够正确解释和处理数据。网络字节序规定使用大端序作为标准字节序。

  为了确保在网络通信中使用统一的字节序,常见的网络编程库提供了一系列函数来进行字节序转换,以确保数据在传输过程中采用网络字节序。

htons() //将16位整数从主机字节序转换为网络字节序(Host to Network Short)。
htonl() //将32位整数从主机字节序转换为网络字节序(Host to Network Long)。
ntohs() //将16位整数从网络字节序转换为主机字节序(Network to Host Short)。
ntohl() //将32位整数从网络字节序转换为主机字节序(Network to Host Long)。inet_ntoa() // 是一个用于将网络字节序的二进制 IPv4 地址转换为点分十进制字符串表示形式的函数。
inet_addr() //函数用于将点分十进制字符串形式的 IPv4 地址转换为网络字节序的二进制形式。

7. socket套接字

(1)定义
   套接字(Socket)是在网络编程中用于实现通信链接的一种机制。它是网络通信的一种约定和规范,能够实现不同计算机之间的通信和数据交换。通过套接字,计算机可以在网络上发送和接收数据,并建立各种类型的网络连接,如TCP连接、UDP连接等。
   套接字提供了一种类似于文件的接口,使得应用程序可以通过读写套接字来进行数据交换。在进行网络编程时,通过套接字,程序可以指定通信的目的地址和端口,实现数据的发送和接收。套接字是网络编程中非常重要的概念,它为数据在网络中的传输提供了基础支持。
   Socket的工作原理基于客户端-服务端模式,其中一个程序充当客户端,另一个充当服务器。通常,服务器在一个主机上运行并侦听特定的端口,客户端则连接到服务器的IP地址和端口号。

(2)类型
   ①流式 socket(SOCK_STREAM):提供可靠的、面向连接的通信流;它使用 TCP 协议,从而保证了数据传输的正确性和顺序性。
   ②数据报 socket(SOCK_DGRAM):定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议 UDP
   ③原始 socket:原始套接字允许对底层协议如 IP 或 ICMP 进行直接访问,它功能强大但使用较为不便,不常用。

二、TCP 常用API

●头文件

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <arpa/inet.h>

1. socket套接字描述符

返回值:套接字描述符

int socket(int domain, int type, int protocol)
//int domain ::代表一个协议族。AF_INET 决定了要用 ipv4 地址;
//int type:指定IP协议类型,常见的是TCP: SOCK_STREAM。 UDP:SOCK_DGRAM等。
//int protocol:当为 0 时,会自动选择 type 类型对应的默认协议。

2. bind套接字绑定

   将套接字绑定到指定的本地地址上,这样就可以在该地址上进行数据收发操作。通常,在服务器程序中,使用bind()函数将服务器套接字绑定到服务器的IP地址和端口号上,以便客户端可以连接到该地址并与服务器通信。

int bind(int sockfd, struct sockaddr* my_addr, int addrlen);
//int sockfd:socket套接字描述符
//struct sockaddr* my_addr :本地ip地址结构体,包含ip地址、端口等。
// int addrlen: struct sockaddr 结构的大小。

使用举例:

//下述结构体已经在#include <sys/socket.h>头文件中定义,不需要程序员自己定义。
/*struct sockaddr_in {sa_family_t sin_family;     // address family: IP协议类型:AF_INET(IPV4)、AF_INET6(IPV6)。in_port_t sin_port;         // port in network byte order 端口号 struct in_addr sin_addr;    //internet address IP 地址 };struct in_addr {in_addr_t s_addr;  //ip地址
*/
//填写绑定的结构体内容	
sockaddr_in server_info;    //初始化结构体
server_info.sin_family =AF_INET;    //IPV4类型
server_info.sin_port=htons(50000);  //htons()将一个主机字节序端口号转为网络字节序下的端口号。
server_info.sin_addr.s_addr=htonl(INADDR_ANY); //所有人都可以连接。bind(sockfd, (struct sockaddr*)&server_info, sizeof(server_info));  //绑定ip以及端口号

   注意:使用10000以内的端口号可能会与系统服务或其他常见的应用程序发生冲突,导致绑定失败或服务异常。因此,在开发自定义应用程序时,建议选择一个不太常用的端口号,通常在49152到65535之间。

3. listen设置最大排队数

   设置最大排队长度:即多个客户端连接服务端时,当服务端没有接收这个请求,则客户端会进入排队等待中,那么这个排队长度就是最大可以容忍排队客户端的数量。这个并不是客户端连接的最大数量。

int listen(int sockfd, int backlog);
//int sockfd:socket套接字描述符。
// int backlog:设置请求排队的最大长度,当有多个客户端程序和服务端相连时, 使用这个表示可以介绍的排队长度。

4. accept接收客户端请求

监听是否有客户端链接,调用 accpet 之后会一直阻塞,直到有用户连接。
返回值:客户端的文件描述符。

int accept(int sockfd, void *addr, int *addrlen);
//int sockfd:socket套接字描述符。
//void *addr :用于存放客户端的信息
//int *addrlen :接收客户端的信息长度。

举例:

sockaddr_in client_info;    //用于存储连接的客户端的信息
int client_socket;client_socket=accept(sockfd, (struct sockaddr*)&client_info, &(sizeof(client_info)));
printf("客户端的socket:%d, ip:%s, 端口:%d\n",client_socket, client_info.sin_addr, client_info.sin_port);

5. connect连接服务端

用于在客户端与服务器进行端连接。0 :成功。1 :失败。

int connect(int sockfd, struct sockaddr * serv_addr,int addrlen);
//int sockfd:socket()返回的文件描述符。
//struct sockaddr * serv_addr:服务端的地址,通常传递 struct sockaddr_in 结构体类型。
//int addrlen) :serv_addr长度。

6. read读取数据

   成功时:返回实际读取的字节数,这个值可能比请求的字节数少,特别是在读取文件尾或读少于请求字节数的数据时。
   出错时:返回 -1,并将 errno 设置为合适的错误码。

#include <unistd.h>ssize_t read(int fildes, void *buf, size_t nbyte);
//int fildes:文件描述符,标识要读取的文件或其他可读对象(如套接字、管道等)。
//void *buf:读取的数据将存储在此缓冲区中。
//size_t nbyte:缓冲区的大小。

7. write发送数据

   成功时:返回实际写入的字节数。这可能比请求的字节数少,尤其是在写入到非阻塞文件描述符时。
   出错时:返回 -1,并将 errno 设置为合适的错误码。

#include <unistd.h>ssize_t write(int fildes, const void *buf, size_t nbyte);
//int fildes:文件描述符,标识要写入的文件或其他可读对象(如套接字、管道等)。
//const void *buf:要写的数据。
// size_t nbyte:数据的大小。

三、UDP 常用API

●头文件

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <arpa/inet.h>

1. socket套接字描述符

返回值:套接字描述符

int socket(int domain, int type, int protocol)
//int domain ::代表一个协议族。AF_INET 决定了要用 ipv4 地址;
//int type:指定IP协议类型,常见的是TCP: SOCK_STREAM。 UDP:SOCK_DGRAM等。
//int protocol:当为 0 时,会自动选择 type 类型对应的默认协议。

2. bind套接字绑定

   将套接字绑定到指定的本地地址上,这样就可以在该地址上进行数据收发操作。

int bind(int sockfd, struct sockaddr* my_addr, int addrlen);
//int sockfd:socket套接字描述符
//struct sockaddr* my_addr :本地ip地址结构体,包含ip地址、端口等。
// int addrlen: struct sockaddr 结构的大小。

3. recvfrom接收数据

   通过发送方的socket,将数据存储到buff中,并将发送方的地址信息存储起来。

ssize_t recvfrom(int socket, void *restrict buffer, size_t length, int flags, struct sockaddr *restrict address,socklen_t *restrict address_len);/*
int socket :指明用哪个套接字接收数据。
oid *restrict buffer:recvfrom函数将接收到的数据存储在此缓冲区中。
size_t length:这是一个指定接收数据缓冲区大小。
int flags:通常为0.
struct sockaddr *restrict address:用于存储发送方的地址信息。如果不需要发送方的地址信息,可以将其设为NULL。
socklen_t *restrict address_len:存储发送方信息结构体的大小。
*/

例如:

int sock; //发送方的socket
ssize_t size;
char buff[64];  //将接收的消息存到buff中
struct sockaddr_in send_info;    //存储发送方的信息    
socklen_t length = sizeof(send_info);//结构体大小size=recvfrom(sock, buff, sizeof(buff), 0, (struct sockaddr *)&send_info, &length);
if(size < 0){perror("recvfrom error");break;
}
printf("%s:%d  [%s]\n", inet_ntoa(send_info.sin_addr), ntohs(send_info.sin_port), buff);

4. sendto发送数据

   通过自身的socket,将buff中的数据发送给目标地址。

ssize_t sendto(int socket, const void *message, size_t length,int flags, const struct sockaddr *dest_addr,socklen_t dest_len);/*
int socket :指明用哪个套接字发送数据。
const void *message:sendto函数将此缓冲区中的数据发送出去。
size_t length:发送数据的长度。
int flags:通常为0
const struct sockaddr *dest_addr:指定目标地址。这个结构包含了目标的IP地址和端口号。
socklen_t dest_len:目标地址的长度。
*/

举例:

int sock; //自身的socket
char buff[64];
ssize_t size;
struct sockaddr_in goal_info;        //信息目的地//绑定信息目的地的信息
goal_info.sin_family = AF_INET;                 //ip协议类型为 ipv4
goal_info.sin_port   = htons(50000);            //端口号为40000。将小端转为大端
goal_info.sin_addr.s_addr=inet_addr("192.168.2.143");    //电脑的ip地址size=sendto(sock, buff, strlen(buff), 0,(const struct sockaddr *)&goal_info, sizeof(goal_info));
if(size < 0){perror("sendto error");break;
}

三、编程流程

1.TCP编程

在这里插入图片描述

使用例程:

●TCP服务端:
   服务端是固定的,客户端是改变的。只能客户端去连接服务端,不能服务端连接客户端。正常先断开客服端,再断开服务端。

#include <sys/types.h>        
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>//-----------------------------------------------服务端---------------------------
int main(int argc, char **argv)
{	  int ret;int client_sock;  int  server_sock;char receive_data[100]={0};struct sockaddr_in client_info;struct sockaddr_in server_info;socklen_t length;//1.创建socket,ip协议类型,网络协议类型,具体协议//SOCK_STREAM(流式): 使用TCP协议,保证数据传输的正确性和顺序性;//SOCK_DGRAM(数据报):使用UDP协议,不保证数据传输的正确性和顺序性。(速度快)	 server_sock=socket(AF_INET, SOCK_STREAM, 0);if(server_sock < 0){perror("socket error");return -1;}//2.设置服务端 server_info.sin_family = AF_INET;                 //ip协议类型为 ipv4server_info.sin_port   = htons(8888);            //端口号为8888。将小端转为大端。server_info.sin_addr.s_addr=htonl(INADDR_ANY);   //接收所有人的连接,程序在哪作为服务端运行,就填该地的ip地址。//3.绑定端口:绑定到指定的socket,绑定的信息,信息的长度ret= bind(server_sock, (const struct sockaddr *)&server_info,sizeof(server_info));if(ret < 0){perror("bind error");return -1;}//4.开启监听:监听的socket,最大排队数(当1个客户端申请连接,但服务端还没有同意连接请求时,客户端会等待。最多有10个可以等待排队的客户端)ret=listen(server_sock, 10);if(ret < 0){perror("listen error");return -1;}//5. 接收客户端连接。length=sizeof(client_info);client_sock= accept(server_sock, (struct sockaddr *)&client_info,  &length); //将连接的客户端的信息存到client_info中。	printf("客户端的socket:%d, ip:%s, 端口:%d\n",client_sock, inet_ntoa(client_info.sin_addr), client_info.sin_port);//6. 服务端向指定的客户端发送数据write(client_sock, "hello world",strlen("hello world"));//7. 接收指定的客户端发送的数据。读不到会阻塞。read(client_sock, receive_data,100);printf("Client:%d,Send_data:%s\n",client_sock, receive_data);//7. 关闭描述符close(client_sock);                //先关闭客户端描述符close(server_sock);                //再关闭服务端描述符
}

在调试时:服务端在哪运行就填哪的IP地址。
在这里插入图片描述

●TCP客户端:

#include <sys/types.h>        
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>int main(int argc, char **argv)
{int client_sock;int ret;struct sockaddr_in server_info;char receive_data[100]={0};// 1.创建socket,ip协议类型,网络协议类型,具体协议//SOCK_STREAM(流式): 使用TCP协议,保证数据传输的正确性和顺序性;//SOCK_DGRAM(数据报):使用UDP协议,不保证数据传输的正确性和顺序性。(速度快)client_sock=socket(AF_INET, SOCK_STREAM, 0);if(client_sock< 0){perror("socket error");return -1;}	  // 2.设置服务端的信息	  server_info.sin_family = AF_INET;                           //ip协议类型为 ipv4server_info.sin_port   = htons(8888);                      //服务端端口号为40000。将小端转为大端server_info.sin_addr.s_addr= inet_addr("192.168.195.15");     //写入服务端的ip地址// 3.连接服务端	  ret=connect(client_sock,(const struct sockaddr *)&server_info, sizeof(server_info));if(ret < 0){perror("connect error");return -1;}printf("connect ok!\n");//4. 向服务器发送数据write(client_sock, "i am client_one", strlen("i am client_one"));//5. 接收服务器发送的数据,读不到会阻塞。read(client_sock, receive_data, 100);  printf("receive_data:%s\n",receive_data);//6. 创建接收服务端数据的线程,实现客户端与服务端互相收发close(client_sock);                //关闭服务端通信句柄return 0;
}

我们使用两个命令窗口,先执行服务端的代码,再执行客户端的代码。
在这里插入图片描述

问题

   1. 上述代码中无论是TCP的服务端还是客户端,我们都是用先写在读的方式,因为如果两端都是先读的话,那么当read读不到数据后会一直堵塞。上述代码虽然通信正常,但是我们不可能只发送或者接收一次数据就终止,我们通常要进行多次的收发。所以要加上while语句一直执行,但是我们又不可能两端每次读之前都得进行写操作。那么怎么解决这个问题,使得双方都可以又读又写,而且互不耽误呢?

答:使用多线程!使用主线程写数据,子线程读数据。而且TCP的服务端,可以由多个客户端连接它,所以使用线程是必不可少的。可以使用多个线程来创建TCP的客户端。多线程参考文章。

   2. 为什么主线程进行写,子线程读。而不是主线程读,子线程写呢?
答:如果只有一个子线程,那么谁写谁读都可以。但是如果多个线程,那么无论是TCP的客户端还是服务端,必须使用主线程来进行写操作。因为我们写的操作是通过键盘来进行输入的,如果使用子线程来进行写操作的话,多个子线程会同时抢占键盘的控制权,假设我们输入了10个字符,可能这10个字符已经被多个线程分割完,所以为了避免这种情况,我们必须要使用主线程来进行写操作。

优化

   既然我们解决了之前存在的问题,那么现在我们优化我们的代码。这里我们还使用了线程的分离属性,具体详情查看《线程》这篇文章中的标题四。

●TCP客户端

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <stdlib.h>int Sock;  //定义全局变量,线程共享。//接收
void *task(void *arg)
{int ret;char receive_data[100] = {0};while (1){memset(receive_data, 0, sizeof(receive_data));    ret = read(Sock, receive_data, sizeof(receive_data) - 1);  // 修正读取长度if (ret <= 0) {if (ret == 0) {printf("Server closed the connection\n");} else {perror("read error");}break;}receive_data[ret] = '\0';  // 确保字符串以NULL结尾printf("Received: %s\n", receive_data);}pthread_exit(NULL);
}int main(int argc, char **argv)
{int ret;pthread_t thread;pthread_attr_t attr;  //线程的分离属性struct sockaddr_in server_info;  //服务端的信息char send_data[100] = {0};// 1.创建socket,ip协议类型,网络协议类型,具体协议Sock = socket(AF_INET, SOCK_STREAM, 0);if (Sock < 0) {perror("socket error");return -1;}// 2.设置服务端的信息    server_info.sin_family = AF_INET;  //ip协议类型为 ipv4server_info.sin_port = htons(8888);  //服务端端口号,将小端转为大端server_info.sin_addr.s_addr = inet_addr("192.168.195.15");  //写入服务端的ip地址// 3.连接服务端    ret = connect(Sock, (const struct sockaddr *)&server_info, sizeof(server_info));if (ret < 0) {perror("connect error");close(Sock);  // 确保在出错时关闭套接字return -1;}printf("connect ok!\n");// 4. 初始化线程属性pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);  //设置子线程分离属性,子线程退出后自动销毁。// 5. 创建线程ret = pthread_create(&thread, &attr, task, NULL);if (ret != 0) {perror("pthread_create error");close(Sock);pthread_attr_destroy(&attr);return -1;}while (1){ // 向服务器发送数据memset(send_data, 0, sizeof(send_data));if (fgets(send_data, sizeof(send_data), stdin) == NULL) {perror("fgets error");break;}ret = write(Sock, send_data, strlen(send_data));if (ret < 0) {perror("send error");break;}}printf("Socket %d 号客户端断开连接\n", Sock);close(Sock);  // 关闭服务端通信句柄pthread_attr_destroy(&attr);  // 销毁线程属性return 0;
}

●TCP服务端

#include <sys/types.h>        
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>typedef struct{                 //用于存储客户端信息int sock;struct sockaddr_in info;
}myclient;void *func(void *arg)                     //多个线程使用同一个函数
{   myclient client;char buff[64];int ret;memcpy(&client, arg, sizeof(client));  //把传来的客户端信息拷贝给client。printf("IP:%s-%d [connect]\n", inet_ntoa(client.info.sin_addr), client.info.sin_port); while(1){memset(buff,0,sizeof(buff));	   ret=read(client.sock, buff, sizeof(buff));   if(ret == 0){                      //客户端断开连接printf("IP:%s-%d [disconnect]\n", inet_ntoa(client.info.sin_addr), client.info.sin_port);close(client.sock);break;}printf("[%s-%d]:%s\n",inet_ntoa(client.info.sin_addr),client.info.sin_port,buff);                         //将接收客户端的buff打印出来}	pthread_exit(NULL);
}int main(int argc, char **argv)
{	  int ret;int  sock_client;   int  sock_server;struct sockaddr_in client_info;struct sockaddr_in server_info;myclient client;    //用于拷贝客户端的信息,并将其作为线程参数传递到线程中。socklen_t length;//1.创建socket,ip协议类型,网络协议类型,具体协议sock_server=socket(AF_INET, SOCK_STREAM, 0);if(sock_server < 0){perror("socket error");return -1;}
//2. 避免先断开程序后,端口被占用报错。int opt=1;setsockopt(sock_server, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));//3.设置服务端 server_info.sin_family = AF_INET;                 //ip协议类型为 ipv4server_info.sin_port   = htons(8888);            //端口号为8888。将小端转为大端server_info.sin_addr.s_addr=htonl(INADDR_ANY);    //接收所有人的连接,程序在哪作为服务端运行,就填该地的ip地址。//4 .绑定端口:绑定到指定的socket,绑定的信息,信息的长度ret= bind(sock_server, (const struct sockaddr *)&server_info,sizeof(server_info));if(ret < 0){perror("bind error");return -1;}//5.开启监听:监听的socket,最大排队数(当1个客户端申请连接,但服务端还没有同意连接请求时,客户端会等待。最多有10个可以等待排队的客户端)ret=listen(sock_server, 10);if(ret < 0){perror("listen error");return -1;}
//6.设置线程的分离属性。pthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);	while(1){ length=sizeof(client_info);//7. 阻塞等待接收客户端连接请求。客户端的socket=(服务端的socket,客户端的信息,信息的长度)	memset(&client_info,0,sizeof(client_info)); 	 sock_client = accept(sock_server, (struct sockaddr *)&client_info,  &length); //将连接的客户端的信息存到client_info中。if(sock_client < 0){perror("sock_client error");return -1;}//8. 将客户端信息拷贝给结构体,将其作为线程传递的参数。memset(&client,0,sizeof(client));  //清除结构体里的内容	   client.sock=sock_client;client.info=client_info;//9.创建线程pthread_t thread;ret= pthread_create(&thread, &attr, func ,(void *)&client);if(ret < 0){perror("pthread_create error");return -1;}}close(sock_client);                //关闭客户端通信句柄close(sock_server);                //关闭服务端通信句柄pthread_attr_destroy(&attr);       // 销毁线程属性
}

经过优化,我们的服务端端可以连接多个客户端,并接收多个客户端发来的信息。
在这里插入图片描述

●问题:那么又有一个小问题,我们先看优化后的TCP服务端代码中创建线程时,传入给线程函数的参数部分。这里我们传入了定义的结构体的地址。那么在之前的文章《线程的创建》中的《线程创建相关问题与解决方法》不是说传入地址的话,那么这个变量改变时,不是会影响所有线程接收的值嘛?

   答:没错,是会影响所有线程接收到的值,但是我们看优化后的TCP代码部分,我们在创建线程前,必须有客户端的连接才会创建线程。多个客户端创建连接时,并不是同一时间进行的。所以当下一个客户端进行连接时,上一个传入线程函数的值已经接收完毕。所以线程间不会相互影响。而下面这种方式创建的线程,这种创建方式是特别快的,所以当上一个传入线程函数的值还没有接收到时,i的值已经发生了变化,所以会影响到上一个线程函数接收到的值。
在这里插入图片描述

●TCP服务端作为中转站

  实现功能:TCP的服务端作为多个客户端发送消息的中转站,即通过服务端将客户端A的数据发送给客户端。这里我们使用链表连接多个客户端。详情:链表知识点。

link.c

#include "link.h"
//创建头节点,头节点不存储信息。
list_node *create_link(void)
{list_node *head;head=(list_node *)malloc(sizeof(list_node));head->next=NULL;return head;
}//添加节点
void add_link(list_node *head, myclient client)
{list_node *new_link=(list_node *)malloc(sizeof(list_node)); ;  //要插入的新节点new_link->client_info = client;new_link->next = NULL;while( head->next!=NULL ){ head =head->next;   //寻找插入位置}head->next =new_link;
}//删除节点
void sub_link(list_node *head, int value)
{list_node *sub_link=(list_node *)malloc(sizeof(list_node));sub_link=head->next;while(sub_link->client_info.sock!= value){ sub_link = sub_link->next;   //寻找插入位置head =head->next;}head->next= sub_link ->next;free(sub_link);  //释放该节点之前malloc申请的内存空间。
}//根据值,查找节点,如果没有值与其相等则返回NULL。
list_node *find_link(list_node *head, int value)
{list_node *p=head->next;while(p!=NULL && p->client_info.sock != value){p=p->next;}return p;
}

link.h

#ifndef __LINK_H
#define __LINK_H//存储客户端信息的结构体
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>typedef struct{int sock;struct sockaddr_in info;
}myclient;   //链表
typedef struct Link{myclient client_info;   //要存储的数据类型struct Link *next;
}list_node;list_node *create_link(void);
void add_link(list_node *head, myclient client);
void sub_link(list_node *head, int value);
list_node *find_link(list_node *head, int value);#endif

TC服务端作为中转站代码:

#include <sys/types.h>        
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "link.h"list_node *head;  //头节点
int client_cout=0;    //连接到服务端的 客户端的数量void *func(void *arg)                     //多个线程使用同一个函数      
{   myclient client;char buff[64];int ret;int sock_num;char data[64];memcpy(&client,arg,sizeof(client));      //拷贝printf("socker:%d-%s [connect]\n",client.sock, inet_ntoa(client.info.sin_addr));printf("当前客户端连接总数:%d\n",client_cout);while(1){memset(buff,0,sizeof(buff));	   ret=read(client.sock, buff, sizeof(buff));   if(ret == 0){printf("socker:%d-%s [disconnect]\n",client.sock, inet_ntoa(client.info.sin_addr));sub_link(head, client.sock);client_cout--;printf("当前客户端连接总数:%d\n",client_cout);close(client.sock);break;}if(client_cout >= 2) {  //客户端连接大于2ret=sscanf(buff,"%d:%s", &sock_num, data); if(ret !=0){      //有输入list_node *p =find_link(head,sock_num);if(p!=NULL)  write(p->client_info.sock, data, strlen(data));else printf("发送失败!请输入有效的socket\n");}}}pthread_exit(NULL);
}int main(int argc, char **argv)
{int sock_server;int sock_client;int ret;struct sockaddr_in server_info;struct sockaddr_in client_info;socklen_t length;char buff[64];myclient client;
//创建socket,ip协议类型,网络协议类型,具体协议sock_server =socket(AF_INET, SOCK_STREAM, 0);if(sock_server < 0){perror("socket error");return -1;}//避免先断开程序后,端口被占用报错。int opt=1;setsockopt(sock_server , SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));//设置服务端server_info.sin_family = AF_INET;                 //ip协议类型为 ipv4server_info.sin_port   = htons(40000);            //端口号为40000。将小端转为大端server_info.sin_addr.s_addr=htonl(INADDR_ANY);    //接收所有人的连接//绑定端口,绑定到指定的socket,绑定的信息,信息的长度ret= bind(sock_server, (const struct sockaddr *)&server_info, sizeof(server_info));if(ret < 0){perror("bind error");return -1;}//开启监听,监听的socket,最大排队数(同一时间连上排队数)ret=listen(sock_server, 10);if(ret < 0){perror("listen error");return -1;}
//创建链表头节点head=create_link();while(1){ length=sizeof(client_info);//阻塞等待接收客户端连接请求,对方的socket,服务端的socket,对方的消息,信息的长度memset(&client_info,0,sizeof(client_info)); sock_client = accept(sock_server, (struct sockaddr *)&client_info,  &length);if(sock_client < 0){perror("sock_client error");return -1;}client.sock=sock_client;client.info=client_info;add_link(head, client);client_cout++;                  //又连接一个客户端//设置分离属性,当线程退出时,自动释放其占用的资源 pthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//创建线程pthread_t thread;ret= pthread_create(&thread, &attr, func ,&client);if(ret < 0){perror("pthread_create error");return -1;}			}close(sock_client);         //关闭客户端通信句柄close(sock_server);        //关闭服务端通信句柄
}

编写好代码后,利用Makefile生成可执行文件运行。Makefile操作详情。

在这里插入图片描述

2.UDP编程

   UDP是无连接的协议,发送数据前不需要建立连接。这使得UDP比TCP更轻量级,适用于需要快速传输数据的场景。UDP没有客户端与服务端之分,只是发送和接收方。

   举例:下面的代码是将发送和接收写在了同一个代码中,当然可以将发送和接收的函数分开。即发送方只发送,接收方只接收。

#include <sys/types.h>        
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>void *func(void *arg)
{int sock=*(int *)arg;ssize_t size;char buff[64];struct sockaddr_in addr;        socklen_t len = sizeof(addr);while(1){   memset(buff, 0, sizeof(buff));//发送方的socket,因为主线程中while用于发送数据。size=recvfrom(sock, buff, sizeof(buff), 0, (struct sockaddr *)&addr, &len);if(size < 0){perror("recvfrom error");break;}printf("%s:%d  [%s]\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port), buff);} pthread_exit(NULL);
}int main(int argc, char **argv)
{int sock;  //同一个套接字 sock 同时具备接收和发送数据的功能。struct sockaddr_in self_info;      int ret;char buff[64];  //存储发送的数据ssize_t size; 
//----------------创建套接字,用于发送和接收。---------------// 创建UDP套接字sock=socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){perror("socket error");return -1;}
//--------------------------用于接收数据----------------------	//设置自身的信息( 接收端才需要绑定自身信息,发送端可以不绑定)self_info.sin_family = AF_INET;                 //ip协议类型为 ipv4self_info.sin_port   = htons(50000);            //端口号为40000。将小端转为大端self_info.sin_addr.s_addr=htonl(INADDR_ANY);    //接收所有人的连接,本地地址。  ret= bind(sock, (const struct sockaddr *)&self_info, sizeof(self_info));if(ret < 0){perror("bind error");return -1;}//创建线程   ,实现同时收发pthread_t thread;ret =pthread_create(&thread, NULL, func ,(void *)&sock);  //因为只有一个线程,所以可以取地址。if(ret < 0){perror("pthraed_create error");return -1;}
//------------------------下面用于发送数据------------------	//设置 发送信息目的地的地址struct sockaddr_in goal_info;  goal_info.sin_family = AF_INET;                 //ip协议类型为 ipv4goal_info.sin_port   = htons(50000);            //端口号为40000。将小端转为大端goal_info.sin_addr.s_addr=inet_addr("192.168.195.148");    //电脑的ip地址while(1){ memset(buff, 0, sizeof(buff));fgets(buff, sizeof(buff), stdin);//发送内容。udp用sendto ,tcp用send.      -------- 虚拟机发送size=sendto(sock, buff, strlen(buff), 0,(const struct sockaddr *)&goal_info,sizeof(goal_info));if(size < 0){perror("sendto error");break;}}close(sock);                //关闭服务端通信句柄
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/341454.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

240.搜索二维矩阵

题目描述 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。每列的元素从上到下升序排列。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,…

【AI大模型】基于Langchain和Openai借口实现英文翻译中文应用

&#x1f680; 作者 &#xff1a;“大数据小禅” &#x1f680; 文章简介 &#xff1a;本专栏后续将持续更新大模型相关文章&#xff0c;从开发到微调到应用&#xff0c;需要下载好的模型包可私。 &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; 目…

CSPM.pdf

PDF转图片 归档&#xff1a;

C盘清理攻略!!!详细步骤

c盘爆满怎么清&#xff0c;往下看 一、清缓存文件键盘winr打开运行窗口&#xff0c;输入&#xff1a;%temp% 二、清理安装包文件键盘winr打开运行窗口&#xff0c;输入&#xff1a;softwaredistribution 三、清理软件解压临时文件键盘winr打开运行窗口&#xff0c;输入&#xf…

【C语言】结构体(及位段)

你好&#xff01;感谢支持孔乙己的新作&#xff0c;本文就结构体与大家分析我的思路。 希望能大佬们多多纠正及支持 &#xff01;&#xff01;&#xff01; 个人主页&#xff1a;爱摸鱼的孔乙己-CSDN博客 欢迎 互粉哦&#x1f648;&#x1f648;&#xff01; 目录 1. 声明结构…

SQL注入-时间盲注

SQL时间盲注&#xff08;Time-based Blind SQL Injection&#xff09;&#xff0c;又叫延时注入&#xff0c;是一种SQL注入攻击技术&#xff0c;用于在无法直接获取查询结果或查看响应内容变化的情况下&#xff0c;通过引入时间延迟来推断数据库的信息&#xff1b;时间盲注依赖…

tinyrenderer-切线空间法线贴图

法线贴图 法线贴图分两种&#xff0c;一种是模型空间中的&#xff0c;一种是切线空间中的 模型空间中的法线贴图的rgb代表着每个渲染像素法线的xyz&#xff0c;与顶点坐标处于一个空间&#xff0c;图片是五颜六色的。 切线空间中的法线贴图的rgb同样对应xyz&#xff0c;是切线…

可视化数据科学平台在信贷领域应用系列四:决策树策略挖掘

信贷行业的风控策略挖掘是一个综合过程&#xff0c;需要综合考虑风控规则分析结果、效果评估、线上实时监测和业务管理需求等多个方面&#xff0c;以发现和制定有效的信贷风险管理策略。这些策略可能涉及贷款审批标准的调整、贷款利率的制定、贷款额度的设定等&#xff0c;在贷…

低代码开发平台一般都有哪些功能和模块?

在当今快速变化的数字化时代&#xff0c;企业对于高效、灵活且经济的软件开发解决方案的需求愈发迫切。低代码开发平台应运而生&#xff0c;成为众多企业实现数字化转型的首选工具。本文将详细探讨低代码开发平台一般具备的主要功能和模块&#xff0c;以及它们如何助力企业提升…

Dinky MySQLCDC 整库同步到 Doris

资源&#xff1a;flink 1.17.0、dinky 1.0.2、doris-2.0.1-rc04 问题&#xff1a;Cannot deserialize value of type int from String &#xff0c;detailMessageunknowndatabases &#xff0c;not a valid int value 2024-05-29 16:52:20.136 ERROR org.apache.doris.flink.…

AI论文工具推荐

AI 在学术界的使用情况也比较疯狂&#xff0c;特别是一些美国大学&#xff0c;用 AI 来辅助阅读文献以及辅助写论文的越来越多&#xff0c;毕竟确实可以提高写作效率&#xff0c;特别是在文献综述和初稿生成方面。 但在科研界其实&#xff0c;发现看论文的速度已经赶不上发论文…

领夹麦克风什么牌子好?2024无线领夹麦克风十大品牌排行榜推荐

​如今&#xff0c;无线麦克风已逐渐渗透到我们日常生活的各个角落&#xff0c;无论是专业的自媒体创作者、带货主播&#xff0c;还是日常拍摄记录生活的我们&#xff0c;都可能用到它。在挑选无线麦克风时&#xff0c;收音降噪效果和性价比无疑是两大核心考量因素。为此&#…

【wiki知识库】05.分类管理实现--前端Vue模块

&#x1f4dd;个人主页&#xff1a;哈__ 期待您的关注 目录 一、&#x1f525;今日目标 二、&#x1f30f;前端部分的改造 2.1 新增一个tool.ts 2.2 新增admin-categoty.vue 2.3 添加新的路由规则 2.4 添加the-welcome.vue 2.5 修改HomeView.vue 三、❗注意 一、&…

The authenticity of host ‘github.com (20.205.243.166)‘ can‘t be established.

目录 github初始化仓库&#xff0c;无法链接 解决无法与主机github.com(20.205.243.166)建立真实性 # 问题原因 # 生成密钥 # 物理路径 # 建立交互 # 验证 github初始化仓库&#xff0c;无法链接 在github创建一个新的仓库时&#xff0c;如果我们未初始化&#xff0c;…

面试题vue+uniapp(个人理解-面试口头答述)未编辑完整....

1.vue2和vue3的区别&#xff08;vue3与vue2的区别&#xff08;你不知道细节全在这&#xff09;_vue2和vue3区别-CSDN博客&#xff09;参考 Vue3 在组合式&#xff08;Composition &#xff09;API&#xff0c;中使用生命周期钩子时需要先引入&#xff0c;而 Vue2 在选项API&am…

操作失败——后端

控制台观察&#xff0c;页面发送的保存菜品的请求 返回的response显示&#xff1a; ---------- 我开始查看明明感觉都挺正常&#xff0c;没啥错误&#xff0c;就是查不出来。结果后面电脑关机重启后&#xff0c;隔一天看&#xff0c;就突然可以了。我觉着可能是浏览器的缓存没…

2022.9.26DAY678

课程学习&#xff1a;《数据处理技术》讲了“数据查询”的语法格式&#xff0c;语法格式也算是简单&#xff0c;就是没能跟之前的内容联系起来&#xff0c;之前的内容没有及时回顾。 高等数学&#xff1a;“ 函数的概念”&#xff0c;讲了函数的概念&#xff0c;反函数&#…

登录通用解决方案 —— 第三方登录处理

目录 01: 前言 02: 第三方平台登录解决方案流程大解析 03: QQ 开放平台流程大解析 04: QQ 登录对接流程&#xff1a;获取 QQ 用户信息 05: QQ 登录对接流程&#xff1a;跨页面信息传输 06: QQ 登录对接流程&#xff1a;认证是否已注册&#xff0c;完成 QQ …

今日科普:了解、预防、控制高血压

高血压&#xff0c;常被称为“隐形的健康威胁”&#xff0c;许多患者可能在毫无预警的情况下发病&#xff0c;且患病率逐年攀升&#xff0c;同时患者群体逐渐年轻化&#xff0c;高血压虽然难以根治&#xff0c;但并不可怕&#xff0c;真正可怕的是血压长期居高不下&#xff0c;…

MySQL中所有常见知识点汇总

存储引擎 这一张是关于整个存储引擎的汇总知识了。 MySQL体系结构 这里是MySQL的体系结构图&#xff1a; 一般将MySQL分为server层和存储引擎两个部分。 其实MySQL体系结构主要分为下面这几个部分&#xff1a; 连接器&#xff1a;负责跟客户端建立连 接、获取权限、维持和管理…