2.1.系统及网络编程——网络基础巩固及Socket网络编程(一)
Socket编程基础
TCP/IP协议的四层
网络接口层(数据链路层,链路层),网络层,传输层,应用层
运输层协议概述
TCP: 传输控制协议,面向连接,可靠的数据传输协议,不会丢失数据
UDP: 用户数据报协议,无连接,不可靠的数据传输协议,可能丢失一些数据,但是较为简便
进程寻址
在应用层完成程序后,希望通过运输层传递数据,运输层类似邮差的功能,为了申请邮差,需要用到套接字socket
socket是进程和运输层之间的接口,进程要发送数据,必须经由socket,交给运输层去交付
头部格式
TCP头部格式:
- 源端口号:16位
- 目标端口号:16位
- 序列号:32位(随机生成)
- 确认应答:32位(希望收到的下一个序列号)
- 首部长度:4位
- 保留:6位,URG(紧急指针有效),ACK(确认序号有效),PSH(接收方应该尽快将这个报文传递给应用层),RST(重置连接),SYN(发起一个新连接),FIN(释放一个连接)
- 三次握手:以客户端发起连接为例
- 客户:SYN = 1,序列号 = x(一般为1),进入SYN-SENT阶段
- 服务器:SYN = 1,,ACK = 1,序列号 = y,确认号 = x + 1,进入SYN-RCVD阶段
- 客户:ACK = 1,序列号 = x +1,确认号 = y + 1,结束SYN-SENT阶段
- 四次挥手:以客户端主动发起释放连接为例
- 客户端:FIN = 1,序列号 = u
- 服务器端:ACK = 1,序列号 = v,确认号 = u + 1
- 服务器端:FIN = 1,ACK = 1,序列号 = w,确认号 = u + 1
- 客户端:ACK = 1,序列号 = u + 1,确认号 = w + 1
- 三次握手:以客户端发起连接为例
- 窗口大小:16位
- 校验和:16位
- 紧急指针:16位
- 选项:长度可变
- 数据
UDP头部格式:
- 源端口号:16位
- 目标端口号:16位
- 包长度:16位
- 校验和:16位,检查内容的正确性
- 数据
代码操作
socket()创建套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
从系统中申请创建一个文件,作为运输层和应用层之间的接口,返回文件描述符,失败为-1,一般用fd表示
- 文件描述符0,1,2分别对应标准输入,标准输出,标准错误输出
domain(域): AF_INET(ipv4),AF_INET6(ipv6),AF_LOCAL(本地域),AF_ROUTE(路由域)
type: SOCK_STREAM(TCP流),SOCK_DRGAM(UDP流)
protocol(协议): 一般为0,因为前两个参数一般都可以决定了协议类型
bind()绑定IP和端口
#include <sys/socket.h>
#include <sys/types.h>
int bind(int sockfd, const struct sockaddr *addr, socketlen_t addrlen);
返回值-1出错,0正常
sockfd: 调用socket返回的文件描述符
addr: 指向数据结构sockaddr的指针,它保存在你的地址(即端口和IP地址)信息
addrlen: 设置为sizeof(struct sockaddr)
socket相关的结构体
#include <sys/socket.h>
struct sockaddr{
sa_family_t sin_family;
char sa_data[14];
};
struct sockaddr_in{
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr{
uint32_t s_addr;
};
sa_family_t: int,协议组,一般为AF_INET
sa_data: 因为是14位,不够存放十进制ip地址和端口,底层存储不是按照字节为基本单位处理的,所以比较难以使用
sockaddr: 因为sa_data使用不便,一般使用sock_addr_in类型进行代替,在传参时候进行强制类型转换
sock_addr_in: 传参时候注意强制类型转换
与sockaddr相关的函数
#include <arpa/inet.h>
unit32_t htonl(uint32_t hostlong);
unit16_t htons(uint16_t hostshort);
unit32_t ntohl(uint32_t netlong);
unit16_t htohs(uint16_t netshort);
in_addr_t inet_addr(const char *ip)
char *inet_ntoa(struct in_addr in);
htonl: h主机字节序,n网络字节序,l为long(s为short)
主机字节序: 大端/小端模式
网络字节序: 4个字节的32bit值以下面的次序传输:0~7bit,最后是24 ~ 31bit
listen()监听socket
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
错误返回-1,否则返回0
sockfd: socket()返回的套接字文件描述符
backlog: 在进入队列中允许的连接数目
accept()接受连接
#include <sys/types.h>
#include <sys/socket.h>
int accept(itn sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept()用来从sockfd上返回一个新的连接:
- 第一个参数sockfd必须是经由socket(),bind(),listen()函数处理后的socket
- 第二个参数是一个地址,将保存对端地址到该地址中
- 第三个参数是地址长度的地址
- 成功返回一个新的sockfd,否则返回-1
- 是一个阻塞函数
connect()建立连接
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端不需要绑定bind, listen,不需要长期占有确定端口,在运行时候可以随机分配一个端口
- sockfd:系统调用socket()返回的套接字文件描述符
- addr:保存目的地端口和ip地址的数据结构
- 设置为sizeof(struct sockaddr)
- 错误返回-1,否则返回0
- connect和accept是一对,分别在客户端和服务端执行,在此期间,完成了三次握手操作
send()发送数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sentto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
发送数据
- sockfd:想要发送数据的套接字描述符
- msg:指向想发送的数据指针
- len:数据的长度
- flags:设置为0
- sentto:主要用于UDP通信中
- dest_addr:为远端要通信的网络地址
- addlen:地址的长度
- 返回成功发送的字节数,如果错误则返回-1,并设置errno
recv()接受数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t addrlen);
接受数据
- sockfd:要读的套接字描述符
- buf:要读的信息的缓冲
- len:缓冲的最大长度
- flags:设置为0
- 返回实际读入缓冲的字节数,出错时候返回-1,同时设置errno
close()关闭连接
#include <unistd.h>
int close(int fd);
没什么好说的
Socket编程例子
服务器使用的是阿里云,需要将端口开放,进入阿里云控制台,在本实例安全组中加入安全组规则,设置端口号
封装函数
以下函数对于服务端,实现套接字的创建,并完成连接和监听,参数为端口号
int socket_create(int port){
int sockfd;
//注意括号数量和对应,判断正确部分的值
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
return -1;
}
//填写表单
struct sockaddr_in server;
server.sin_family = AF_INET;
//主机字节库转换为网络字节库
server.sin_port = htons(port);
//暴力方法,直接设为0。为32位整型
server.sin_addr.s_addr = 0;
//注意传递时候要进行强制类型转换,传递参数为结构指针
if (bind(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0){
return -1;
}
if(listen(sockfd, 10) < 0){
return -1;
}
return sockfd;
}
服务器端
Sever端代码
int main(int argc, char **argv){
if(argc != 2){
fprintf(stderr, "Usage : %s port!\n", argv[0]);
exit(1);
}
int server_listen, port, sockfd;
port = atoi(argv[1]);
if((server_listen = socket_create(port)) < 0){
perror("socket_create()");
exit(1);
}
if((sockfd = accept(server_listen, NULL, NULL)) < 0){
perror("accept()");
exit(1);
}
char buff[512] = {0};
recv(sockfd, buff, sizeof(buff), 0);
printf("%s\n", buff);
send(sockfd, &buff, strlen(buff), 0);
close(sockfd);
close(server_listen);
return 0;
}
注意编译时候要加上相应头文件,以及封装的socket_create()
函数,参考的编译语句为gcc server.c ../common/common.c -I ../common/
客户端
代码如下
int main(int argc, char **argv){
if(argc != 3){
fprintf(stderr, "Usage:%s ip port\n", argv[0]);
exit(1);
}
char ip[20] = {0};
int port, sockfd;
strcpy(ip, argv[1]);
port = atoi(argv[2]);
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
perror("socket()");
exit(1);
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip);
if(connect(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0){
perror("connect()");
exit(1);
}
char buff[512] = {0};
sprintf(buff, "Hello World!");
send(sockfd, buff, strlen(buff), 0);
bzero(buff, sizeof(buff));
recv(sockfd, buff, sizeof(buff), 0);
printf("Server Echo : %s\n", buff);
return 0;
}
注意补充相应头文件进行编译
执行结果
如下图所示,在8888端口中客户端首先发送字符串,然后服务端接收字符串并打印,之后发回给客户端,客户端打印服务端传来数据
Tips1:
有什么不知道函数,或者查寻函数所在的库,man手册查找下
- -f:查找命令
- -k:查找相关命令