端口地址转换
htonl / htons
// 主机字节序(host)到网络字节序(network)
#include <arpa/inet.h>
u_long htonl (u_long hostlong); //long 4byte - 端口号一般为2byte, 基本用不到这个函数
u_short htons (u_short short); //short 2byte - 端口号2byte, 一般使用这个函数
ntohl / ntohs
// 网络字节序(network)到主机字节序(host)
#include <arpa/inet.h>
u_long ntohl (u_long hostlong);
u_short ntohs (u_short short); //short 2byte - 端口号2byte, 一般使用这个函数
函数功能实现:
IP 地址转换
inet_addr
// 主机字节序 ——> 网络字节序
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef uint32_t in_addr_t; // 通过 vi -t 追一下
struct in_addr {
in_addr_t s_addr;
};
in_addr_t inet_addr(const char *strptr);
功能: 主机字节序(点分十进制形式的IP) 转为网络字节序。
参数: const char *strptr: 字符串
返回值: 成功:返回一个无符号长整型数(无符号32位整数用十六进制表示);
失败:NULL
inet_ntoa
// 网络字节序 ——> 主机字节序
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
char *inet_ntoa(stuct in_addr inaddr);
功能: 将网络字节序二进制地址转换成主机字节序。
注: 根据 TCP/IP 协议,IP地址 用二进制表示,每个 IP地址 的长度为 32位
参数: stuct in_addr in addr: 只需传入一个结构体变量
返回值: 成功:返回一个字符指针;
失败:NULL;
TCP 编程
流程图
实现步骤
服务器端(server):
1)socket,创建流式套接字文件,用于连接 sockfd(有一个属性默认是阻塞) SOCK_STREAM;
2)填充结构体: 填充 自己的 IP 和 端口
3)bind,绑定,把 socket() 函数返回的 文件描述符和 自己的 IP、端口号 进行绑定;
4)listen,监听,将 socket() 返回的文件描述符,由主动套接字变为被动套接字;
5)accept,阻塞函数,阻塞等待客户端的连接请求,成功将返回一个只用于通信的套接字文件;
6)recv(read),接收客户端发来的数据; || send(write),发送数据;
7)close,关闭文件描述符(至少要关闭:连接、通信)。
客户端(client):
1)socket,创建流式套接字文件,既用于连接,也用于通信;
2)填充结构体: 填充 服务器的 IP 和 端口
3)connect,用于发起连接请求,阻塞等待连接服务器;
4)send,发送数据; || recv,接收数据;
5)close,关闭文件描述符。
函数
socket 服务器、客户端创建套接字
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建套接字
参数:
domain:协议族
AF_UNIX, AF_LOCAL 本地通信
AF_INET ipv4
AF_INET6 ipv6
type:套接字类型
SOCK_STREAM:流式套接字
SOCK_DGRAM:数据报套接字
protocol:协议 - 填 0 自动匹配底层 ,根据 type 系统默认自动帮助匹配对应协议
传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
返回值:
成功:文件描述符 0->标准输入 1->标准输出 2->标准出错 3->socket
失败:-1,更新 errno
bind 服务器(、客户端)绑定套接字
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:绑定 协议? ip? 端口 ? 让别人识别
参数:
sockfd:套接字
addr:用于通信结构体 (提供的是通用结构体,需要根据选择通信方式,填充对应结构体-
通信当时socket第一个参数确定,需要强转)
(结构体之间的强转,不可以直接使用普通数据类型强转,
因为结构体字节对齐原则,具体大小不一致,会数据丢失,
所以可使用结构体指针,对结构体地址进行强转)
addrlen:结构体大小
返回值: 成功: 0
失败:-1,更新 errno
/********************************************************************/
通用结构体:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
ipv4 通信结构体:
struct sockaddr_in {
sa_family_t sin_family; ----协议族
in_port_t sin_port; ----端口
struct in_addr sin_addr; ----ip结构体
};
struct in_addr {
uint32_t s_addr; --ip地址
};
本地通信结构体:
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX 本地通信 */
char sun_path[108]; /* 在本地创建的套接字路径 */
};
listen 服务器端监听
int listen(int sockfd, int backlog);
功能:监听,将主动套接字变为被动套接字
参数:
sockfd: 套接字
backlog:同一时间可以响应客户端请求链接的最大个数,不能写 0.
不同平台可同时链接的数不同,一般写6-8个
返回值:成功:0 失败:-1,更新 errno
connect 客户端连接服务器
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:用于连接服务器;
参数:
sockfd: socket函数的返回值
addr: 填充的结构体是服务器端的;
addrlen: 结构体的大小
返回值:
成功:0
失败:-1,更新 errno
accept 服务器端阻塞等待连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:阻塞函数,阻塞等待客户端的连接请求,
如果有客户端连接,则accept()函数返回一个用于通信的套接字文件;
参数:
sockfd :套接字
addr: 链接客户端的ip和端口号
如果不需要关心具体是哪一个客户端,那么可以填 NULL;
addrlen:结构体的大小
如果不需要关心具体是哪一个客户端,那么可以填 NULL;
返回值:
成功:文件描述符; // 用于通信
失败:-1,更新errno
send 客户端 ⇆ 服务器 发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送数据
参数:
sockfd: socket函数的返回值
buf: 发送内容存放的地址
len: 发送内存的长度
flags: 如果填 0,相当于write();
返回值: 成功:发送的字节数
失败:-1
recv 客户端 ⇆ 服务器 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 接收数据
参数:
sockfd: acceptfd;
buf: 存放位置
len: 大小
flags: 一般填 0,相当于read()函数
MSG_DONTWAIT 非阻塞
返回值:
< 0 失败出错 更新errno
==0 表示客户端退出
> 0 成功接收的字节个数
server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
// 1. 创建套接字 socket 如果成功,则返回一个用于链接的文件描述符
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
printf("sockfd: %d\n", sockfd); // sockfd == 3
// 1.5 填充结构体
struct sockaddr_in saddr;
saddr.sin_family = AF_INET; // 填充协议族
saddr.sin_port = htons(8888); // 填充端口号
saddr.sin_addr.s_addr = inet_addr("192.168.0.103"); // 填充IP地址
// 2. bind 绑定端口和 ip
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("Failed to bind");
return -1;
}
// 3. 监听 listen 6个客户端上限
if (listen(sockfd, 6) < 0){
perror("Failed to listen");
return -1;
}
// 4. 阻塞等待客户端的链接 accept,返回一个用于与客户端通信的文件描述符
int acceptfd = accept(sockfd, NULL, NULL); // NULL 表示不关心连接的客户端
if (acceptfd < 0){
perror("Failed to accept");
return -1;
}
printf("acceptfd: %d\n", acceptfd); // vscode 内显示 5,黑色终端内显示 4
// 5. 接收来自客户端的信息
char buf[256] = {};
while (1){
int recvbyte = recv(acceptfd, buf, sizeof(buf), 0); // 阻塞
if (recvbyte < 0){
perror("Failed to receive");
return -1;
} else if (recvbyte == 0){
printf("Client exited\n");
break;
} else {
printf("%s\n", buf);
}
}
close(acceptfd);
close(sockfd);
return 0;
}
client.c
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
// 1. 创建 socket 套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
// 1.5 填充结构体
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = inet_addr("192.168.0.103");
// 2. 链接
if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("Failed to connect");
return -1;
}
// 3. 发送 或 接收
char buf[256] = {};
while (1)
{
fgets(buf, sizeof(buf), stdin);
// if (buf[strlen(buf)-1] == '\n')
// buf[strlen(buf)-1] = '\0';
send(sockfd, buf, sizeof(buf), 0);
}
close(sockfd);
return 0;
}
实现效果如下:
注意:
1、先运行 client.c 时会报错
2、未运行 client.c 时,server.c 阻塞等待
3、使用 fgets() 函数时,应避免以下情况
4、先结束运行 client.c,server.c 正常退出;先结束运行 server.c,client.c 等待
进一步优化
1、去掉 fgets() 获取的多余的’\n’
fgets() 函数实际读到的内容小于等于指定个数-1,读到的内容后强制添加’\0’,会将’\n’也读入。
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf)-1] == '\n') // 去掉 fgets 获取的'\n'
buf[strlen(buf)-1] = '\0';
2、端口 和 IP地址 通过命令行传参到代码中
#include <stdlib.h>
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr(argv[1]);
执行时:gcc xxxx.c -o xxx
./xxx <IP地址> <端口号>
3、设置客户端退出,服务器结束循环接收
通过 recv的返回值为0 判断客户端是否退出。
4、设置来电显示功能,获取到请求链接服务器的客户端的IP和端口
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
// 打印时记得使用 inet_ntoa() 和 ntohs() 将网络字节序转换为主机字节序:
printf(“…”, ntohs(), inet_ntoa());
struct sockaddr_in caddr;
socklen_t length = sizeof(caddr);
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &length);
if (acceptfd < 0){
perror("Failed to accept");
return -1;
}
printf("acceptfd: %d\n", acceptfd);
printf("Client IPv4: %s\tport: %d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
5、设置服务器端自动获取 自己的IP地址
INADDR_ANY == inet_addr(“0.0.0.0”)
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1])); // 记得修改参数下标
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
6、实现循环服务器,服务器不退出,当链接服务器的客户端退出,服务器等到下一个客户端链接。
// 在 accept 处使用 while
struct sockaddr_in caddr;
socklen_t length = sizeof(caddr);
while (1){
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &length);
if (acceptfd < 0){
perror("Failed to accept");
return -1;
}
printf("acceptfd: %d\n", acceptfd);
printf("Client IPv4: %s\tport: %d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
char buf[256] = {};
while (1){
int recvbyte = recv(acceptfd, buf, sizeof(buf), 0); // 阻塞
if (recvbyte < 0){
perror("Failed to receive");
return -1;
} else if (recvbyte == 0){
printf("Client exited\n");
break;
} else {
printf("%s\n", buf);
}
}
close(acceptfd);
}
close(sockfd);
7、当客户端输入 quit 的时候,客户端和服务器服务器都要退出
server.c
// ./ser 8888
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (argc != 2){
printf("Please input %s <port>. \n", argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 0 表示自动匹配相应的协议
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
printf("sockfd: %d\n", sockfd);
struct sockaddr_in saddr; // 服务器端
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("Failed to bind");
return -1;
}
if (listen(sockfd, 6) < 0){
perror("Failed to listen");
return -1;
}
struct sockaddr_in caddr; // 客户端
socklen_t length = sizeof(caddr);
int flag = 0;
while (1){
if (flag)
break;
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &length);
if (acceptfd < 0){
perror("Failed to accept");
return -1;
}
printf("acceptfd: %d\n", acceptfd);
printf("Client IPv4: %s\tport: %d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
char buf[256] = {};
while (1){
int recvbyte = recv(acceptfd, buf, sizeof(buf), 0); // 0 表示阻塞
if (recvbyte < 0){
perror("Failed to receive");
return -1;
} else if (recvbyte == 0){
printf("Client exited\n");
break;
} else {
if (!strcmp(buf, "quit")){
flag = 1;
break;
}
printf("%s\n", buf);
}
}
close(acceptfd);
}
close(sockfd);
return 0;
}
client.c
// ./cli <IP地址> 8888
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (argc != 3){
printf("Please input %s <ip> <port>. \n", argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 0 表示自动匹配相应的协议
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr(argv[1]);
if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("Failed to connect");
return -1;
}
char buf[256] = {};
while (1){
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] = '\0';
send(sockfd, buf, sizeof(buf), 0); // 0 表示阻塞
if (!strcmp(buf, "quit"))
break;
}
close(sockfd);
return 0;
}
实现效果见“进一步优化:7”。