小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
1 TCP网络编程
1.1 模型
C/S模型:客户端、服务器模型
B/S模型:浏览器、服务器模型
1.2 流程
服务器:
- 创建套接字 socket()
- 填充服务器网络信息结构体 struct sockaddr_in
- 将套接字与服务器网络信息结构体绑定 bind()
- 将套接字设置为被动监听状态 listen()
- 阻塞等待客户端的连接 accept()
- 进行通信 read()/write()、recv()/send()、recvfrom()\sendto()
客户端:
- 创建套接字 socket()
- 填充服务器网络信息结构体 struct sockaddr_in
- 给服务器发送连接请求 connect()
- 进行通信 read()/write()、recv()/send()、recvfrom()\sendto()
inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
atoi:将数字型字符串转换为整形数据
htons:将主机字节序转化为网络字节序
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
struct sockaddr_in {
sa_family_t sin_family; /* 地址族: AF_INET */
in_port_t sin_port; /* 网络字节序的端口号 */
struct in_addr sin_addr; /* ip地址 */
-->
struct in_addr {
uint32_t s_addr; /* 网络字节序的无符号4字节整数ip地址 */
};
};
1.3 函数讲解
1.3.1 socket()
- 头文件:
#include <sys/types.h>#include <sys/socket.h>
- 原型:
int socket(int domain, int type, int protocol); - 功能:创建套接字,返回一个文件描述符
- 参数:
- domain:通信域,协议族
- AF_UNIX 本地通信
- AF_INET ipv4网络协议
- AF_INET6 ipv6网络协议
- AF_PACKET 底层协议通信
- type:套接字的类型
- SOCK_STREAM 流式套接字 --TCP
- SOCK_DGRAM 数据报套接字 --UDP
- SOCK_RAW 原始套接字
- protocol:附加协议,传0表示不需要其他协议
- domain:通信域,协议族
- 返回值:
- 成功:文件描述符
- 失败:-1
1.3.2 bind()
-
头文件:
#include <sys/types.h>#include <sys/socket.h>
-
原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); -
功能:将套接字与网络信息结构体绑定
-
参数:
- sockfd:文件描述符,
- socket的返回值
- addr:网络信息结构体
通用结构体:一般不用 struct sockaddr { sa_family_t sa_family; char sa_data[14]; }网络信息结构体:通过==man 7 ip查询==
struct sockaddr_in { sa_family_t sin_family; /* 地址族: AF_INET */ in_port_t sin_port; /* 网络字节序的端口号 */ struct in_addr sin_addr; /* ip地址 */ --> struct in_addr { uint32_t s_addr; /* 网络字节序的无符号4字节整数ip地址 */ }; };- addrlen:addr的大小
-
返回值:
-
成功:0
-
失败:-1
-
1.3.3 listen()
- 原型:
#include <sys/types.h>#include <sys/socket.h>
- 原型:int listen(int sockfd, int backlog);
- 功能:将套接字设置为被动监听状态
- 参数:
- scokfd:文件描述符,
- socket的返回值
- backlog:允许同时连接的客户端的个数,一般设置为5,10
- 返回值:
- 成功:0
- 失败:-1
1.3.4 accept()
- 头文件:
#include <sys/types.h>#include <sys/socket.h>
- 原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); - 功能:阻塞等待客户端的连接
- 参数:
- sockfd:文件描述符,socket的返回值
- addr:被填充的网络信息结构体,如果有客户端连接服务器,服务器可以通过这个参数获取客户端的信息 ,设置为NULL我服务器根本就不接收。
- addrlen:addr的大小
- 返回值:
- 成功:文件描述符
- 失败:-1
1.3.5 connect()
- 头文件
#include <sys/types.h>#include <sys/socket.h>
- 原型:
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); - 功能:给服务器发送连接请求
- 参数:
- sockfd:文件描述符,
- socket的返回值
- addr:要连接的服务器的网络信息结构体,需要客户端自己填充
- addrlen:addr的大小
- 返回值:
- 成功:0
- 失败:-1
1.3.6 字节序转换
#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);
#include <arpa/inet.h>
#include <stdio.h>
int main(int argc, const char *argv[])
{
unsigned int a = 0x12345678;
unsigned int b = htonl(a);
printf("%#x --> %#x\n", a, b);
return 0;
}
1.3.7 网络字节序和IP地址
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
将点分十进制字符串ip地址转化为网络字节序的无符号4字节整数ip地址
in_addr_t inet_addr(const char *cp);
将网络字节序的无符号4字节整数ip地址转化为点分十进制字符串ip地址
char *inet_ntoa(struct in_addr in);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
int main(int argc, char const *argv[])
{
unsigned char ip_str[] = "192.168.70.71";
unsigned int ip_int;
ip_int = inet_addr(ip_str);
printf("ip_int = %u\n", ip_int);
unsigned char *p = (unsigned char *)&ip_int;
printf("%d.%d.%d.%d\n", *p, *(p+1), *(p+2), *(p+3));
return 0;
}
1.4 功能实现(两个demo)
1.4.1 服务器功能实现
//TCP网络变成值服务器
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket error");
exit(1);
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
//atoi:将数字型字符串转换为整形数据
//htons:将主机字节序转化为网络字节序
serveraddr.sin_family = AF_INET;
//注意:ip地址不能随便写,服务器在那个主机中运行,ip地址就是这个主机的
//如果是自己主机的客户端服务器测试,可以使用127网段的
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
perror("bind error");
exit(1);
}
//第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
perror("listen error");
exit(1);
}
//第五步:阻塞等待客户端的连接
if(accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen) == -1)
{
perror("accept error");
exit(1);
}
//打印客户端的信息
printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
return 0;
}
1.4.2 客户端功能实现
//TCP网络编程之客户端
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket error");
exit(1);
i, }
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:给服务器发送客户端的连接请求
if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
perror("connect error");
exit(1);
}
return 0;
}
2 TCP客户端服务器通信
2.1 recv()/send()
2.1.1 -- recv()
-
头文件:
#include <sys/types.h>#include <sys/socket.h>原型:ssize_t recv(int sockfd, void *buf, size_t len, int flags);
-
功能:接收数据
-
参数:
-
sockfd:文件描述符
- 服务器:accept的返回值
- 客户端:socket的返回值
-
buf:保存接收到的数据
-
len:理论要接收的字节数
-
flags:标志位
-
0 阻塞
-
MSG_DONTWAIT 非阻塞
-
-
-
返回值:
-
成功:实际接收到的字节数
-
失败:
-
-1
-
0:发送方关闭文件描述符或者退出,接收方recv会返回0
-
-
read(sockfd, buf, N)
<==>
recv(sockfd, buf, N, 0)
<==>
recvfrom(sockfd, buf, N, 0, NULL, NULL)
2.1.2 -- send()
- 头文件:
- #include <sys/types.h>
- #include <sys/socket.h>
- 原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- 功能:发送数据
- 参数:
-
sockfd:文件描述符
- 服务器:accept的返回值
- 客户端:socket的返回值
-
buf:要发送的数据
-
len:理论要发送的字节数
-
flags:标志位
-
0 阻塞
-
MSG_DONTWAIT 非阻塞
-
-
- 返回值:
- 成功:实际发送的字节
- 失败:-1
2.2 代码(两个demo)
2.2.1 服务器代码
//TCP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd, acceptfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {0};
ssize_t bytes;
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
//atoi:将数字型字符串转换为整形数据
//htons:将主机字节序转化为网络字节序
serveraddr.sin_family = AF_INET;
//注意:ip地址不能随便写,服务器在那个主机中运行,ip地址就是这个主机的
//如果是自己主机的客户端服务器测试,可以使用127网段的
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr,
addrlen) == -1)
{
ERRLOG("bind error");
}
//第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
NEXT:
//第五步:阻塞等待客户端的连接
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) == -1)
{
ERRLOG("accept error");
}
//打印客户端的信息
printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
//进行通信
while(1)
{
if((bytes = recv(acceptfd, buf, N, 0)) == -1)
{
ERRLOG("recv error");
}
else if(bytes == 0)
{
printf("客户端%s-%d退出了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
goto NEXT;
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端%s-%d退出了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
goto NEXT;
}
printf("%s-%d:%s\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), buf);
strcat(buf, "^_^");
//如果发送放关闭,当send执行第二次的时候会结束当前进程
//是因为第二次执行send的时候产生了SIGPIPE信号,这个信号默认对当前进程
//的处理方式是结束进程
if(send(acceptfd, buf, N, 0) == -1)
{
ERRLOG("send error");
}
}
return 0;
}
2.2.2 客户端
//TCP网络编程之客户端
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:给服务器发送客户端的连接请求
if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("connect error");
}
//进行通信
char buf[N] = {0};
while(1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
if(send(sockfd, buf, N, 0) == -1)
{
ERRLOG("send error");
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端退出了\n");
exit(0);
}
memset(buf, 0, N);
if(recv(sockfd, buf, N, 0) == -1)
{
ERRLOG("recv error");
}
printf("服务器:%s\n", buf);
}
return 0;
}
2.2.3 Makefile
all:server client
server:server.o
client:client.o
%*.o:%*.c
.PHONY:clean
clean:
$(RM) *.o server client core a.out
3 TCP三次握手和四次挥手
TCP三次握手主要指的是TCP连接的过程
三次握手主要是在客户端的connect和服务器端的listen、accept函数之间完成的
TCP四次挥手主要指的是TCP断开连接的过程
四次挥手主要实在客户端服务器退出或者关闭文件描述符的时候完成的