我们之前学的通信是本地进程间通信,如果我们想在网络间通信的话,就需要用到二者的ip地址,分别被称为源IP地址和目的IP地址,被存入ip数据包中,其次我们还需要遵循一些通信协议。
TCP协议:传输层协议,有连接,可靠传输,面向字节流(无明确边界的字节序列),难
UDP协议:传输层协议,无连接,不可靠传输,面向数据流(以数据包为单位),简单
网络通信的本质也是进程间通信,你怎么知道你要和哪个进程通信?
端口号
端口号(port)是传输层协议的内容
端口号是一个2字节16位的整数
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
一个端口号只能被一个进程占用
IP+PORT表示互联网中唯一的一个进程,这样就可以锚定是哪个进程了
IP+PORT就是socket(套接字)
如果socket可以表示进程,那pid和socket是什么关系?
并不是所有进程都可以实现网络通信,有端口号的进程才是需要网络通信的进程;如果不加入端口号只用pid标识的话,系统和网络之间强耦合很容易出问题
一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定。
因为端口号也是有分类的,不同的端口号负责的部分不同
0~1023是比较知名的端口号,HTTP,FTP,SSH这些应用层协议都有固定端口号
1024~65535是操作系统动态分配的端口号,客户端程序的端口号就是由操作系统从这个范围分配的
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号,就是在描述 数据是谁发的, 要发给谁
网络字节序
Linux:线程-CSDN博客这篇里我们提到了页表内对于地址的索引,是分大小端的
大端序:高字节存储在低地址,低字节存储在高地址。
小端序:低字节存储在低地址,高字节存储在高地址。
磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分
如何定义收到的网络数据流地址?
发送主句通常将发送缓冲区的数据按内存地址从低到高的顺序发出;接受主机把从网络上接到的字节保存在接收方的缓冲区内,也是按内存地址从低到高的顺序保存。
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节. 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络 字节序和主机字节序的转换
h:主机字节序
n:网络字节序
l:32位长整数
s:16位短整数
htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
cpp
inet_addr()
#include <arpa/inet.h>
in_addr_t inet_addr(const char *ip);
功能:是将一个点分十进制ipv4的IP地址转换32位大端网络字节序整数
参数:点分十进制的ip地址字符串ip
返回值:成功时返回32位大端整数,失败返回INADDR_NONE
socket
常用API
cpp
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
接口结构:
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同,为了适用多种协议,才有多种结构
1、通信有网络通信和本地通信,下图的sockaddr就是用来判断是网络通信还是本地通信的
2、IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址
3、IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
4、socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主 要有三部分信息: 地址类型, 端口号, IP地址
cpp
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
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
cpp
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命 名
功能:绑定,将fd 和本地的IP + 端口进行绑定
参数:
sockfd : 通过socket函数得到的文件描述符
addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
addrlen : 第二个参数结构体占的内存大小
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
cpp
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
功能:监听这个socket上的连接
参数:
sockfd : 通过socket()函数得到的文件描述符
backlog : 未连接的和已经连接的和的最大值, 5
作为服务端需要时刻监听是否有客户端发来的数据,服务端就是调用listen()来监听建立的socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
cpp
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能: 客户端连接服务器
参数:
sockfd : 用于通信的文件描述符
addr : 客户端要连接的服务器的地址信息
addrlen : 第二个参数的内存大小
返回值:成功 0, 失败 -1
客户端通过调用connect函数来建立与TCP服务器的连接
cpp
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
参数:
sockfd : 用于监听的文件描述符
addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
addrlen : 指定第二个参数的对应的内存大小
返回值:
成功 :用于通信的文件描述符
-1 : 失败
服务器侧在调用socket()、bind()、listen()之后,就会监听指定的socket地址了。客户端在调用socket()、connect()之后就建立了一条连接通道并发向服务端发送一个请求,服务器监听到这个请求之后,就会调用accept()函数取接收请求。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作
发送和接收的接口:
cpp
ssize_t recv(int socket, void *buf, size_t len, int flags)
参数一:指定接收端套接字描述符;
参数二:指向一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
参数三:指明buf的长度;
参数四:一般置为0;
返回值:失败时,返回值小于0;超时或对端主动关闭,返回值等于0;成功时,返回值是返回接收数据的长度。
cpp
ssize_t send(int socket, const void *buf, size_t len, int flags);
参数一:指定发送端套接字描述符;
参数二:指明一个存放应用程序要发送数据的缓冲区;
参数三:指明实际要发送的数据的字节数;
参数四:一般置0;
返回值:失败时,返回值小于0;超时或对端主动关闭,返回值等于0;成功时,返回值是返回发送数据的长度。
查看正在使用该端口的进程
cpp
sudo lsof -i :8888
server.c
cpp
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include<pthread.h>
#define SERVER_PORT 8888
//调用socket函数返回的文件描述符
int serverSocket;
//声明两个socket_in变量,表示客户端和服务端
struct sockaddr_in server_Addr;
struct sockaddr_in client_Addr;
int addr_len = sizeof(client_Addr);//给accept用的
int clientSocket;
char recvbuffer[128]; //存储 发送和接收的信息
int iDataNum;
char sendbuffer[128];
int sockets_create(){
if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
{
perror("socket");
return 1;
}
//初始化
memset(&server_Addr,0,sizeof(server_Addr));
server_Addr.sin_family = AF_INET;
server_Addr.sin_port = htons(SERVER_PORT);
//ip可是是本服务器的ip,也可以用宏INADDR_ANY代替,代表0.0.0.0,表明所有地址
server_Addr.sin_addr.s_addr = htonl(INADDR_ANY);
return 0;
}
//监听
int Listen(){
if(bind(serverSocket, (struct sockaddr *)&server_Addr, sizeof(server_Addr)) < 0)
{
perror("connect");
return 1;
}
//监听,最大数为5
if(listen(serverSocket,5)<0){
perror("Listen");
return 1;
}
printf("监听端口:%d\n",SERVER_PORT);
return 0;
}
//接收/发送消息
void* client_handler(void* client_Socket){
int clientSocket = *(int *)client_Socket;
printf("IP is %s\n", inet_ntoa(client_Addr.sin_addr)); //把来访问的客户端的IP地址打出来
printf("Port is %d\n", htons(client_Addr.sin_port));
while(1){
iDataNum=recv(clientSocket,recvbuffer,1024,0);
if(iDataNum < 0)continue;
recvbuffer[iDataNum]='\0';
if(strcmp(recvbuffer,"quit")==0)break;
printf("client#%s\n",recvbuffer);
printf("server#");
scanf("%s",sendbuffer);
send(clientSocket,sendbuffer,sizeof(sendbuffer),0);
if(strcmp(sendbuffer, "quit") == 0) break;
}
return 0;
}
int main(){
int *new_Socket;//指针类型,和其他客户端指向同一个地址
sockets_create();
Listen();//监听
//循环等待连接,连接上了创建新线程
while(1){
//accept
clientSocket=accept(serverSocket,(struct sockaddr*)&client_Addr, (socklen_t*)&addr_len);
if(clientSocket < 0)
{
perror("accept");
return 1;
}
new_Socket=malloc(sizeof(clientSocket));
*new_Socket=clientSocket;
pthread_t client_pthread;
if(pthread_create(&client_pthread,NULL,client_handler,(void*)new_Socket)<0){
perror("pthread create");
close(clientSocket);
free(new_Socket);
new_Socket=NULL;
continue;
}
printf("Handler assigned\n");
pthread_detach(client_pthread);
}
//msg();
return 0;
}
client.c
cpp
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h> // 包含 inet_pton 的头文件
#include <unistd.h>
#define SERVER_PORT 8888
//定义文件描述符和套接字,用于和服务器通信
int serverSocket;
char SERVER_IP[16];
struct sockaddr_in serverAddr;
int iDataNum;
//定义buffer
char sendbuf[128];
char recvbuf[128];
//创建套接字
int sockets_create(){
if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
{
perror("socket");
return 1;
}
//初始化
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(SERVER_PORT);
printf("[DEBUG] 转换 IP 地址: %s\n", SERVER_IP);
if(inet_pton(AF_INET, SERVER_IP, &serverAddr.sin_addr) <= 0) {
perror("inet_pton error");
//serverAddr.sin_addr.s_addr=inet_pton(SERVER_IP);inet_addr()函数,将点分十进制IP转换成网络字节序IP(感觉和pton没什么区别)
return 1;
}
printf("[DEBUG] 转换 IP 地址 成功");
return 0;
}
//连接服务端
int connect_ser(){
printf("connecting...\n");
if(connect(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0)
{
perror("connect");
return 1;
}
printf("连接到主机%s...\n",SERVER_IP);
}
//发送/接收信息
int msg(){
while(1){
printf("client#");
scanf("%s",sendbuf);
send(serverSocket, sendbuf, strlen(sendbuf), 0); //向服务端发送消息
if(strcmp(sendbuf, "quit") == 0) break;
printf("server#:");
recvbuf[0] = '\0';
iDataNum = recv(serverSocket, recvbuf, 1024, 0); //接收服务端发来的消息
if(iDataNum < 0)continue;
recvbuf[iDataNum] = '\0';
printf("%s\n", recvbuf);
}
close(serverSocket);
}
int main(){
printf("please send host\n");
scanf("%s",SERVER_IP);
sockets_create();
connect_ser();
msg();
return 0;
}
这就很抽象,首先我们实现了多个client和一个server的对话,但是可以看出,client端只能一收一发,不能连发多条
如果想实现边收边发我觉得我们需要再线程里创建线程,使读和写并发
下次再改,现在我要睡觉了,电脑没电了