1 IP地址
1.1 IP地址的基本理解
IP协议有两个版本,IPv4 和IPv6,我们这里使用IPv4。
- IPv4有4个字节,32位来表示
- IP地址四IP协议中用来标识网络中不同主机的地址
- 我们通常使用点分十进制标识IP地址,如192.168.1.0
1.2 源IP地址和目的IP地址
在IP数据报头部有两个IP地址,一个是源IP地址,表示该数据报从哪个主机发送过来;一个是目的IP地址,表示该数据报需要发送给哪个目的主机。这样,我们就能够清楚地发送数据到对应的地方了。
可是到了对应的地方,应该将数据交给哪个人进行使用呢?这时候就需要端口号了。
2 端口号
2.1 初识端口号
端口号是传输层协议的内容:
- 端口号是一个2字节16位的一个整数
- 端口号用来标识一个进程,告诉操作系统,当前的数据应该交由哪一个进程来处理
- IP地址+端口号可以唯一标识某一个主机的一个进程
- 一个端口号只能被一个进程占用
2.2 进程ID与端口号
简单来说,就是没有什么关系,这是两个不同的概念:
- 端口号+IP地址可以唯一标识整个网络上的一个进程;而进程ID只能唯一标识该主机上的进程;
- 端口号是为了传输数据而存在的;进程ID 是为了进程调度而存在的;
2.3 源端口号和目的端口号
类似于源IP地址和目的IP地址,
源端口号表示从那一个进程发送的数据;目的端口号表示要将数据发送至哪一个进程。
端口号和IP地址组合起来就能唯一标识网络上的一个进程,就能准确的知道要将数据发送到那里去,该数据从哪里来。
3 简单认识TCP/UDP协议
3.1 TCP协议
TCP(Transmission Control Protocol传输控制协议)是一个传输层的协议,它是一个有连接,面向字节流的可靠传输。
TCP用于可靠传输的情况,应用于文件的传输,重要状态更新等场景。
3.2 UDP 协议
UDP(User Datagram Protocol ⽤户数据报协议)也是一个传输层的协议,它是一个无连接,面向数据报的不可靠传输。
UDP协议用于对高速传输和实时性要求较高的通信领域,例如:早期的QQ,视频传输等,另外UDP也可以用于广播。
4 网络字节序
我们知道,内存中存放数据有大端小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分,那么在数据传输的过程中如何保证数据传输的顺序呢?
发送主机通常将发送缓冲区的数据按内存地址从低到高的顺序发出;
接受主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的发送顺序是这样的:先发送的数据是低地址,后发送的数据是高地址;
我们可以这样设想,如果发送方式大端序,接收方是小端序,那么发送方发送的低地址的数据在接收方也是低地址,发送方发送的高地址的数据在接收端也存放在高地址处,这样一来,如果按照接收端主机的小端方式对数据进行解析,就会造成接收到的数据与发送的数据并不相同的情况。这时候,就需要一个网络序来规范它们,在发送前将主机序转换为网络序,在接收后将网络序转换为主机序。所以:
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节;
不管这台主机是大端还是小端,都按照TCP/IP协议规定的网络字节序来发送数据;
如果当前发送主机是小端,就需要先将数据转成大端;否则,就忽略,直接发送即可;
以下是C语言中主机序与网络序转换的常用接口:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
5 socket编程接口
// 创建 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);
6 使用UDP实现一个简单的服务器
实现一个回显的服务器,也就是说不对收到的请求做出计算,直接发送回给客户端。
//server
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("usage: ./server [IP] [port]\n");
return -1;
}
//启动(创建socket,并且绑定IP和port
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
perror("socket");
return 2;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]); //将点分十进制的转换成数字,并且是网络字节序
addr.sin_port = htons(atoi(argv[2]));
int ret = bind(sock, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return 2;
}
//进入循环,从客户端读取数据,经过计算,将结果写回到客户端
while(1)
{
char buf[1024] = {0};
struct sockaddr_in client;
socklen_t len;
int64_t s = recvfrom(sock, buf, sizeof(buf)-1, 0, (struct sockaddr*)&client, &len);
if(s < 0)
{
perror("recvfrom");
continue;
}
buf[s] = '\0';
printf("[%s:%d] %s\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port), buf);
sendto(sock, buf, strlen(buf), 0, (struct sockaddr*)&client, sizeof(client));
}
close(sock);
return 0;
}
//client
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//用户输入数据
//将数据发送给服务器
//读取返回结果
//将结果打印到标准输出上
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("usage: ./client [IP] [port] \n");
return 0;
}
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
close(sock);
return -1;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]); //将点分十进制IP地址转化为网络字节序的IP地址
server.sin_port = htons(atoi(argv[2])); //将端口号从网络序转为主机序
//需要注意的是,这里不需要绑定IP及port。
//如果客户端主动绑定一个端口号,会有什么问题呢?
//
//进入循环,从客户端读数据发送给服务器端
while(1)
{
printf(">");
fflush(stdout);
char buf[1024] = {0};
int s = read(0, buf, sizeof(buf)-1);
if(s < 0)
{
perror("read");
close(sock);
return 1;
}
if(s == 0)
{
printf("read done\n");
close(sock);
return 0;
}
buf[s] = '\0';
sendto(sock, buf, strlen(buf), 0, (struct sockaddr*)&server, sizeof(server));
//已经知道了服务器端的IP地址和端口号,所以此时并不关注
//因为收到的数据一定来自服务器
int64_t rs = recvfrom(sock, buf, sizeof(buf)-1, 0, NULL, NULL);
if(rs > 0)
{
buf[rs] = '\0';
printf("echo:%s\n", buf);
}
}
return 0;
}
7 使用TCP 实现一个简单的服务器
同样是实现一个回显服务器:
如果只有一个执行流,那么就只能同时处理一个客户端发送的请求,因为只accept了一次;为了能使一个服务器可以连接多个客户端,我们可以使用多线程,或是多进程的方式,将对请求的处理交给子进程或是其他线程来完成,让父进程或是主线程进行循环的accept。
//server(多线程版本)
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
void* Entry(void* args)
{
int new_sock = (int)args;
//循环处理数据
while(1)
{
char buf[1024] = {0};
int s = read(new_sock, buf, sizeof(buf));
if(s > 0)
{
write(new_sock, buf, strlen(buf)+1);
}
}
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("usage: ./server [IP] [port]\n");
return 2;
}
//创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
//绑定IP及port
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(atoi(argv[2]));
int ret = bind(sock, (struct sockaddr*)&server, sizeof(server));
if(ret < 0)
{
perror("bind");
return 2;
}
//使用listen允许服务器被客户端连接
if(listen(sock, 128) < 0)
{
perror("listen");
return 2;
}
//服务器初始化完成,进入事件循环
//为了能够连接多个客户端,处理不同客户端发来的请求,这里使用多线程的方式
while(1)
{
struct sockaddr_in client;
int len = sizeof(client);
int new_sock = accept(sock, (struct sockaddr*)&client, &len);
if(new_sock < 0)
{
close(sock);
continue;
}
pthread_t td;
int ret = pthread_create(&td, NULL, Entry, new_sock);
pthread_detach(td);
}
return 0;
}
//server(多进程版本)
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <signal.h>
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("usage: ./server [IP] [port]\n");
return 2;
}
//创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
//绑定IP及port
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(atoi(argv[2]));
int ret = bind(sock, (struct sockaddr*)&server, sizeof(server));
if(ret < 0)
{
perror("bind");
return 2;
}
//使用listen允许服务器被客户端连接
//第二个参数代表允许等待的客户端的个数,不需要太大
if(listen(sock, 128) < 0)
{
perror("listen");
return 2;
}
//服务器初始化完成,进入事件循环
//为了能够连接多个客户端,处理不同客户端发来的请求,这里使用多线程的方式
while(1)
{
struct sockaddr_in client;
int len = sizeof(client);
int new_sock = accept(sock, (struct sockaddr*)&client, &len);
if(new_sock < 0)
{
close(sock);
continue;
}
printf("client %d connect\n", new_sock);
int fd = fork();
if(fd > 0)
{
//father
waitpid(fd, NULL, 0);
close(new_sock);
}
else if(fd == 0)
{
//child
while(1)
{
char buf[1024] = {0};
int s = read(new_sock, buf, sizeof(buf));
if(s > 0)
{
write(new_sock, buf, strlen(buf)+1);
}
exit(0);
}
}
else
{
perror("fork");
exit(0);
}
}
return 0;
}
//client
#include <stdio.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("usage: ./client [IP] [port]\n");
return 2;
}
//创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("socket");
return 3;
}
//建立连接
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
{
perror("connect");
return 3;
}
//进入循环处理数据
while(1)
{
printf(">");
fflush(stdout);
char buf[1024] = {0};
fgets(buf, sizeof(buf), stdin);
write(sock, buf, strlen(buf)+1);
int s = read(sock, buf, sizeof(buf));
if(s > 0)
{
buf[s] = '\0';
printf("echo:%s\n", buf);
}
}
}