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

本文目录

  • 一、基础知识点
    • [1. IP地址](#1. IP地址)
    • [2. 端口](#2. 端口)
    • [3. 域名](#3. 域名)
    • [4. 网络协议类型](#4. 网络协议类型)
    • [5. IP协议类型](#5. IP协议类型)
    • [6. 字节序](#6. 字节序)
    • [7. socket套接字](#7. socket套接字)
  • 二、常用API
    • [1. socket套接字描述符](#1. socket套接字描述符)
    • [2. bind套接字绑定](#2. bind套接字绑定)
    • [3. listen设置客户端连接个数](#3. listen设置客户端连接个数)
    • [4. accept接收客户端请求](#4. accept接收客户端请求)
    • [5. connect连接服务端](#5. connect连接服务端)
  • 三、编程流程

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

一、基础知识点

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)网络字节序

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

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

c 复制代码
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(套接字)是在网络编程中用于实现不同主机间通信的一种机制,它允许应用程序通过网络发送和接收数据。

Socket的工作原理基于客户端-服务器模型,其中一个程序充当客户端,另一个充当服务器。通常,服务器在一个主机上运行并侦听特定的端口,客户端则连接到服务器的IP地址和端口号。

(2)类型

①流式 socket(SOCK_STREAM):提供可靠的、面向连接的通信流;它使用 TCP 协议,从而保证了数据传输的正确性和顺序性。

②数据报 socket(SOCK_DGRAM):定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议 UDP

③原始 socket:原始套接字允许对底层协议如 IP 或 ICMP 进行直接访问,它功能强大但使用较为不便,不常用。

二、常用API

●头文件

c 复制代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <arpa/inet.h>

1. socket套接字描述符

返回值:套接字描述符

c 复制代码
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地址和端口号上,以便客户端可以连接到该地址并与服务器通信。

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

使用举例:

c 复制代码
//下述结构体已经在#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以及端口号

3. listen设置客户端连接个数

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

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

4. accept接收客户端请求

监听是否有客户端链接,调用 accpet 之后会一直阻塞,直到有用户连接。

返回值:客户端的文件描述符。

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

举例:

c 复制代码
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 :失败。

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

三、编程流程

1.TCP编程

使用例程:

●TCP服务端:

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

c 复制代码
#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协议类型为 ipv4
	server_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客户端:

c 复制代码
#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协议类型为 ipv4
	server_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客户端

c 复制代码
#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 *task(void *arg)
{
  int ret;
  int client_socket =*(int *)arg;
  char receive_data[100]={0};
  free(arg);
  while(1)
  {
 	  memset(receive_data, 0, sizeof(receive_data));	
      ret=read(client_socket, receive_data, strlen(receive_data));
      if(ret == 0){
			perror("send error");
			break;
		}	
	  printf("%s\n",receive_data);
  }
  close(client_socket);
  pthread_exit(NULL);
}

int main(int argc, char **argv)
{
    int ret;
	int *client_sock;
	pthread_t thread;
	struct sockaddr_in server_info;  //服务端的信息
	char send_data[100]={0};
	
	// 1.创建socket,ip协议类型,网络协议类型,具体协议
	client_sock= (int *)malloc(sizeof(int));
	*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协议类型为 ipv4
	server_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");
   
   //创建线程
	ret=pthread_create(&thread, NULL, task,(void *)client_sock);
	if (ret != 0) {
	        perror("pthread_create error");
	        return -1;
	    }
	
   while(1)
   { 
     //4. 向服务器发送数据
		fgets(send_data,sizeof(send_data),stdin);
		ret= write(*client_sock, send_data, strlen(send_data));
		if(ret < 0){
			perror("send error");
			break;
		}	
    }
	printf("Socket%d号客户端断开连接\n",*client_sock);
	close(*client_sock);                //关闭服务端通信句柄
	return 0;
}
相关推荐
MC丶科4 分钟前
【SpringBoot常见报错与解决方案】端口被占用?Spring Boot 修改端口号的 3 种方法,第 3 种 90% 的人不知道!
java·linux·spring boot
我有一颗五叶草5 分钟前
HTTP 协议
网络·网络协议·http
江公望19 分钟前
ubuntu kylin(优麒麟)和标准ubuntu的区别浅谈
linux·服务器·ubuntu·kylin
Lynnxiaowen20 分钟前
今天我们开始学习python语句和模块
linux·运维·开发语言·python·学习
沐风ya42 分钟前
RPC介绍
网络·网络协议·rpc
生态笔记1 小时前
PPT宏代码
linux·服务器·powerpoint
mucheni1 小时前
迅为RK3588开发板Ubuntu 系统开发ubuntu终端密码登录
linux·运维·ubuntu
skywoodsky1 小时前
Ubuntu 24.04环境下的挂起转休眠
linux
OAFD.1 小时前
YOLOv3 详解:核心改进、网络架构与目标检测实践
网络·yolo·目标检测
小云数据库服务专线1 小时前
GaussDB 应用侧报Read timed out解决方法
linux·服务器·gaussdb