2.1.系统及网络编程——网络基础巩固及Socket网络编程(一)

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:查找相关命令
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值