1.IP 地址(Internet Protocol Address)
是用于在互联网或局域网中标识设备的唯一地址。
2.端口(Port)
是计算机网络中用于标识特定服务或应用程序的逻辑接口,它是一个 16 位的数字,范围从 0 到 65535。
3.Socket 编程
是一种网络编程技术,用于在不同设备或进程之间进行通信,Socket(套接字)提供了一个应用程序与网络之间的接口
4.字节序(Byte Order)
是指多字节数据在内存中的存储顺序,它在网络编程中非常重要,因为不同架构的计算机可能使用不同的字节序,而网络协议通常要求使用统一的字节序来确保数据的正确传输和解析
-
小端字节序(Little-Endian):
-
定义:最低位字节存放在内存的最低地址端,最高位字节存放在内存的最高地址端
-
-
大端字节序(Big-Endian):
-
定义:最高位字节存放在内存的最低地址端,最低位字节存放在内存的最高地址端,网络协议规定使用大端字节序作为网络字节序
-
C 语言中用于字节序转换的标准函数
-
htons
:Host to Network Short(将 16 位主机字节序转换为网络字节序) -
htonl
:Host to Network Long(将 32 位主机字节序转换为网络字节序) -
ntohs
:Network to Host Short(将 16 位网络字节序转换为主机字节序) -
ntohl
:Network to Host Long(将 32 位网络字节序转换为主机字节序)
5.IP转化(由操作系统提供的标准网络编程接口)
1. inet_pton
:从字符串到二进制
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
-
af
:地址族,可以是AF_INET
(IPv4)或AF_INET6
(IPv6)。 -
src
:指向 IP 地址字符串的指针(如"192.168.1.1"
或"2001:0db8:85a3:0000:0000:8a2e:0370:7334"
)。 -
dst
:指向存储转换后的二进制 IP 地址的缓冲区的指针。
2. inet_ntop
:从二进制到字符串
inet_ntop
函数用于将 IP 地址从二进制形式转换为字符串形式。
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
-
af
:地址族,可以是AF_INET
(IPv4)或AF_INET6
(IPv6)。 -
src
:指向二进制 IP 地址的指针。 -
dst
:指向存储转换后的字符串的缓冲区的指针。 -
size
:缓冲区的大小。
6.TCP通信
TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
1. TCP 通信的基本流程
TCP 通信过程可以分为以下几个主要阶段:
-
建立连接(三次握手)
-
数据传输
-
关闭连接(四次挥手)
2. 详细流程
1. 建立连接(三次握手)
TCP 是面向连接的协议,在数据传输之前,必须先建立一个可靠的连接。建立连接的过程称为“三次握手”,具体步骤如下:
-
第一次握手(SYN):
-
客户端向服务器发送一个 SYN(同步)报文,请求建立连接。
-
报文中的序列号(Sequence Number)被设置为一个随机值
x
。 -
报文格式:
SYN=1, seq=x
-
-
第二次握手(SYN-ACK):
-
服务器收到客户端的 SYN 报文后,会发送一个 SYN-ACK 报文作为响应。
-
服务器在响应报文中确认客户端的序列号(
ack=x+1
),同时设置自己的序列号为另一个随机值y
。 -
报文格式:
SYN=1, ACK=1, seq=y, ack=x+1
-
-
第三次握手(ACK):
-
客户端收到服务器的 SYN-ACK 报文后,会发送一个 ACK(确认)报文。
-
客户端确认服务器的序列号(
ack=y+1
),完成连接建立。 -
报文格式:
ACK=1, seq=x+1, ack=y+1
-
此时,TCP 连接已经建立,客户端和服务器可以开始传输数据。
2. 数据传输
一旦连接建立,客户端和服务器就可以通过 TCP 连接发送和接收数据。TCP 保证数据的可靠传输,主要通过以下机制:
-
序列号(Sequence Number):每个字节的数据都被分配一个序列号,用于数据的排序和确认。
-
确认应答(ACK):接收方在收到数据后会发送确认应答,告知发送方数据已成功接收。
-
重传机制:如果发送方在一定时间内没有收到确认应答,会重传数据。
-
滑动窗口协议:用于流量控制,确保发送方不会发送过多数据,超出接收方的处理能力。
-
校验和:用于检测数据在传输过程中是否发生错误。
3. 关闭连接(四次挥手)
数据传输完成后,需要关闭 TCP 连接。关闭连接的过程称为“四次挥手”,具体步骤如下:
-
第一次挥手(FIN):
-
客户端向服务器发送一个 FIN(结束)报文,表示客户端已经完成数据发送,请求关闭连接。
-
报文格式:
FIN=1, seq=u
-
-
第二次挥手(ACK):
-
服务器收到客户端的 FIN 报文后,发送一个 ACK 报文作为响应,确认收到 FIN 报文。
-
报文格式:
ACK=1, seq=v, ack=u+1
-
-
第三次挥手(FIN):
-
服务器在完成自己的数据发送后,也会向客户端发送一个 FIN 报文,请求关闭连接。
-
报文格式:
FIN=1, ACK=1, seq=w, ack=u+1
-
-
第四次挥手(ACK):
-
客户端收到服务器的 FIN 报文后,发送一个 ACK 报文作为响应,确认收到 FIN 报文。
-
报文格式:
ACK=1, seq=u+1, ack=w+1
-
此时,TCP 连接已经关闭。
3. TCP 通信的特点
-
面向连接:在数据传输之前,必须先建立连接。
-
可靠传输:通过序列号、确认应答、重传机制和校验和等机制,确保数据的可靠传输。
-
基于字节流:TCP 将数据视为字节流,不保留数据的边界。
-
流量控制:通过滑动窗口协议,确保发送方不会发送过多数据,超出接收方的处理能力。
-
拥塞控制:通过拥塞控制算法,动态调整发送方的发送速率,避免网络拥塞。
4. 示例代码
以下是一个简单的 TCP 客户端和服务器的示例代码(C 语言):
服务器端代码
需要两个描述符一个监听,一个通信
accept: 阻塞函数,没有客户端向服务端发起连接,一直阻塞。如果想和多个客户端建立连接,需要循环。
每个文件描述符有读写两个缓冲区。
read/recv:在读缓冲区没数据时阻塞
write/send:在写缓冲区写满时阻塞
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
// 创建 Socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 开始监听,监听客户端连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 接受连接,address传出参数存储客户端地址信息
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 接收数据
char buffer[1024] = {0};
read(new_socket, buffer, 1024);
printf("收到数据:%s\n", buffer);
// 发送响应
const char *message = "Hello from server";
send(new_socket, message, strlen(message), 0);
// 关闭连接
close(new_socket);
close(server_fd);
return 0;
}
客户端代码
connect:bind+connect,在服务端随机绑定端口(服务器不会主动链接客户端)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sock;
struct sockaddr_in server_address;
// 创建 Socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置服务器地址
server_address.sin_family = AF_INET;
server_address.sin_port = htons(8080);
server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
// 连接到服务器,三个参数都是传入参数,address存储服务端地址信息
if (connect(sock, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {
perror("connection failed");
exit(EXIT_FAILURE);
}
// 发送数据
const char *message = "Hello from client";
send(sock, message, strlen(message), 0);
// 接收响应
char buffer[1024] = {0};
recv(sock, buffer, 1024, 0);
printf("收到响应:%s\n", buffer);
// 关闭连接
close(sock);
return 0;
}
5.多线程实现服务器并发
1. 实现服务器同时处理多个客户端:使用多线程
2. 线程:共享内存,栈是独有的, 同时操作共享资源--->线程同步
3.使用线程池实现服务器并发
进程负责创建socket bind listen 创建线程池,将acceptConn放到任务队列中(acceptConn线程将一直存在负责连接客户端)
acceptConn接收客户端连接后将working放入任务队列中
working负责与客户端通信
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(){
int fd;
struct sockaddr_in addr;
fd = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.115.128", &addr.sin_addr);
addr.sin_port = htons(8080);
printf("connecting>>>\n");
if(connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0){
perror("connect");
return -1;
}
printf("connected>>>\n");
int i = 0;
while(1){
char buf[1024];
sleep(1);
sprintf(buf, "hello %d\n", i++);
if(send(fd, buf, strlen(buf) + 1, 0) != strlen(buf) + 1){
perror("send\n");
return -1;
}
memset(buf, 0, sizeof(buf));
int len = recv(fd, buf, sizeof(buf), 0);
if(len > 0){
printf("recieve: %s\n", buf);
}
else if(len == 0){
printf("unconnect\n");
return -1;
}
else{
printf("recv\n");
return -1;
}
}
close(fd);
return 0;
}
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include "threadpool.h"
void working(void* arg);
void acceptConn(void* arg);
//1. 构建结构体存储传递给线程连接函数
typedef struct PoolInfo{
ThreadPool* pool;
int fd;
}PoolInfo;
//2. 构建结构体存储传递给线程工作函数
struct SockInfo{
struct sockaddr_in addr;
int fd;
};
struct SockInfo infos[5];
int main(){
int fd_listen;
struct sockaddr_in addr;
int addrlen = sizeof(addr);
if((fd_listen = socket(AF_INET, SOCK_STREAM, 0)) < 0){
perror("socket");
return -1;
}
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(8080);
if(bind(fd_listen, (struct sockaddr*)&addr, addrlen) < 0){
perror("bind");
return -1;
}
if(listen(fd_listen, 5) < 0){
perror("listen");
return -1;
}
ThreadPool* pool = threadPoolCreate(3, 8, 100);
PoolInfo* info = (PoolInfo*)malloc(sizeof(PoolInfo));
info->fd = fd_listen;
info->pool = pool;
threadPoolAdd(pool, acceptConn, info);
// close(info->fd);
pthread_exit(NULL);
return 0;
}
void acceptConn(void* arg){
PoolInfo* info = (PoolInfo*)arg;
printf("accepting>>>\n");
while(1){
struct SockInfo* pinfo;
pinfo = (struct SockInfo*)malloc(sizeof(struct SockInfo));
int len = sizeof(pinfo->addr);
// if((pinfo->fd = accept(info->fd, (struct sockaddr*)&pinfo->addr, (socklen_t*)&len)) < 0){
// break;
// }
pinfo->fd = accept(info->fd, (struct sockaddr*)&pinfo->addr, (socklen_t*)&len);
printf("accepted>>>\n");
threadPoolAdd(info->pool, working, pinfo);
}
}
void working(void* arg){
//5. 获得传递参数
struct SockInfo* pinfo = (struct SockInfo*)arg;
printf("working>>>\n");
char ip[32];
printf("client'IP: %s, port: %d\n",
inet_ntop(AF_INET, &pinfo->addr.sin_addr, ip, sizeof(ip)),
ntohs(pinfo->addr.sin_port));
while(1){
char buf[1024];
memset(buf, 0, sizeof(buf));
int len = recv(pinfo->fd, buf, sizeof(buf), 0);
if(len > 0){
printf("receive: %s\n", buf);
write(pinfo->fd, buf, len);
}
else if(len == 0){
printf("end\n");
break;
}
else{
perror("recv");
}
}
close(pinfo->fd);
}