socket 编程

1. socket 套接字

Socket 是一个用于网络通信的技术。Socket 通信允许客户端------服务器之间进行双向通信。它可以使任何客户端机器连接到任何服务器,安装在客户端和服务器两侧的程序就可以实现双向的通信。Socket的作用就是把连接两个计算机的通信软件"中间接"起来,能够实现远程连接

socket 是一个编程接口 (网络编程接口),是一种特殊的文件描述符 (write/read/close)

socket 并不仅限于TCP/IP协议

socket 是独立于具体协议的编程接口,这个接口位于TCP / IP四层模型中的应用层与传输层之间


socket类型:

(1) 流式套接字 (SOCK_STREAM)

面向字节流,针对传输层协议为TCP的应用

保证数据传输是可靠的

提供一种可靠的、面向连接的双向数据传输服务,实现了数据无差错、无重复的发 送。流式套接字内设流量控制,被传输的数据看作是无记录边界的字节流

(2) 数据报套接字 (SOCK_DGRAM)
针对传输层协议为UDP的应用
提供一种无连接的服务,该服务并不能保证数据传输的可靠性

它提供了一种无连接、不可靠的双向数据传输服务。数据在传输过程中可能会丢失或重复,并且不能保证在接收端按发送顺序接收数据

(3) 原始套接字(SOCK_RAW)

直接跳过传输层,该套接字允许对较低层协议 (如IP或ICMP) 进行直接访问,常用于网络协议分析,检验新的网络协议实现,也可用于测试新配置或安装的网络设备

把socket(网络编程接口)当成一个特殊的文件描述符即可

2. TCP网络应用

任何的网络应用都有通信双方:
服务器(Server) / 客户端(Client)

网络结构:CS架构

TCP套接字编程基本流程:

TCP网络应用

TCP Server

TCP Client

任何的网络应用:

IP(目标主机) + 传输层协议(如何传输) + 端口号(具体应用)

TCP网络应用的数据传输的大致过程:

(1) 建立连接:

"三次握手"

(2) 发送/接收网络数据 (操作socket)

write / send / sendto
read / recv / recvfrom

(3) 关闭连接:

"四次挥手"

TCP网络应用编程流程:

TCP Server

(1) socket:创建一个套接字

(2) bind:把一个套接字和网络地址绑定到一起
如果你想要其他人主动来连接你,你就必须bind一个地址,并且把这个地址告诉其他人
注意:不调用bind,并不代表你的socket就没有地址,不管你调不调用bind,socket在通信时,内核都会为你的socket指定一个地址

(3) listen:让套接字进入"监听模式"
(4) 有连接请求的时候

accept 接收一个监听队列上面的请求

多次的调用accept就可以与不同的客户建立连接

accept 在"监听套接字"上,创建一个与客户端的"连接套接字"

(5) 进行通信,读写数据

write / send / sendto

read / recv / recvfrom

(6) 关闭socket套接字:"四次挥手"

close / shutdown

TCP Client
(1) socket:创建一个套接字

(2) connect 主动与server建立连接需要知道服务器的地址 (设置服务器的IP和端口)

(3) 进行通信,读写数据

write / send / sendto

read / recv / recvfrom

(4) 关闭socket套接字:"四次挥手"

close / shutdown

3. socket 具体的API函数解析

(1) socket:创建一个套接字
cpp 复制代码
NAME
    socket - create an endpoint for communication
SYNOPSIS
    #include <sys/types.h>
    #include <sys/socket.h>
		
socket用来创建一个通信端口"socket"
       
int socket(int domain, int type, int protocol);
		
domain:
    指定域,协议族。socket接口不仅仅局限于TCP/IP,还可以用于buletooth,本地进程间通信...
	每一种通信模式下面都有一系列自己的协议,归到一类:协议族
		AF_INET   IPV4协议族
		AF_INET6  IPV6协议族
		AF_UNIX(UNIX域协议)/ AF_LOCAL  本地进程间通信

type:指定要创建的套接字类型
          SOCK_STREAM  流式套接字     TCP
	      SOCK_DGRAM  数据报套接字   UDP
		  SOCK_RAW    原始套接字
		  ...
		
protocol:指定具体的应用层协议,可以指定为0(不知名的私有应用协议)	
			
返回值:
	成功返回一个套接字描述符(>0,特殊的文件描述符)
	失败返回-1,同时errno被设置
(2) bind:把一个套接字和网络地址绑定到一起
cpp 复制代码
NAME
    bind - bind a name to a socket
SYNOPSIS
    #include <sys/types.h>
    #include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);			
		
sockfd:	要绑定的套接字描述符
		
addr:要绑定的网络地址结构体指针
		
addrlen:要绑定的网络地址结构体的长度,单位:字节
	     通过指针去访问内存,为了防止内存越界
		
返回值:
	成功返回0
	失败返回-1,同时errno被设置
(3) 网络地址结构体

socket 描述符可以用于IPV4也可以用于IPV6,也可以用于蓝牙......

但是不同的协议里面,"地址"的描述方法不一样

设置了一个通用的地址结构体,所有的socket 函数接口用到的地址参数的类型都使用:

struct sockaddr****这种类型表示

**所有协议的地址都是使用这个结构体去描述一个地址的,**在这个结构体的第一个成员变量中,指定了协议族,按照相应的协议族去解析具体的地址

cpp 复制代码
通用地址结构体:#include<linux/socket.h>
定义在/usr/include/linux/socket.h
struct sockaddr {
    sa_family_t  sa_family; // 指定协议族
	char         sa_data[14]; // 空间,存放具体协议的地址
	   
};

我们现在使用IPV4,所以这个地址需要使用一个IPV4协议族下面的地址
IPV4的地址结构体:vim /usr/include/netinet/in.h
// #include <netinet/in.h>
struct sockaddr_in { // 描述一个IPV4的地址(IP + 端口号)
    sa_family_t sin_family; // 指定协议族
    u_int16 sin_port; // 端口号,2个字节,必须是网络字节序
    struct in_addr sin_addr; // IPV4地址,整数,4个字节
    unsigned char sin_zero[8]; // 填充8个字节,为了和通用网络地址结构体一样大
};

typedef uint32_t in_addr_t;
struct in_addr { // 32位的IP地址
	in_addr_t s_addr;
};
cpp 复制代码
问题1:
    人类常说的ip是"点分式",但是结构体中要求的却是struct in_addr,怎么办?
        IPV4地址转换函数

//  INADDR_ANY:一个宏,表示任何地址 "0.0.0.0"  
// 一个监听地址,表示服务器愿意接收来自任何客户端的连接请求 

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
		
// inet_aton:把"点分式"的IP地址转换为in_addr类型的地址
       
int inet_aton(const char *cp, struct in_addr *inp);
		
cp:指针,指向你要转换的Ip字符串("点分式")
		
inp:指针,指向一块内存空间,保存转换后的Ip地址
		
返回值:
	成功返回0
	失败返回-1,errno被设置
			
例子:
    // 定义一个IPV4的结构体
	struct sockaddr_in sa;
	memset(&sa, 0, sizeof(sa));
	// 设置协议族为IPV4
	sa.sin_family = AF_INET;	
	// 把"192.168.1.4"这个地址转换后存入sa的成员中
	inet_aton("192.168.1.4", &(sa.sin_addr));
	// sa.sin_addr.s_addr = inet_addr("192.168.1.4");

	// sa就描述了一个IPV4的地址
------------------------------------------------------------------------
// inet_addr是把点分式的IP转换为in_addr_t类型
// 只不过此函数是把转换结果直接返回
       
in_addr_t inet_addr(const char *cp);

sa.sin_addr.s_addr = inet_addr("192.168.1.4");	   
-------------------------------------------------------------------------	
// inet_network与inet_addr功能一样
in_addr_t inet_network(const char *cp);
-------------------------------------------------------------------------	
// 把一个网络地址转换为IPV4的点分式字符串,返回这个字符串的首地址
char* inet_ntoa(struct in_addr in);	

// inet_ntoa(cAddr.sin_addr);
cpp 复制代码
问题2:
    PC上面一般是小端模式,在指定端口号的时候,需要使用大端模式(网络字节序)
        网络字节序与主机字节序之间的转换

NAME
    htonl,htons,ntohl,ntohs - convert values between host and network byte order
SYNOPSIS
    #include <arpa/inet.h>

h: host 主机字节序
n: network 网络字节序
l: long -->32bits
s: short --->16bits	

// 将字符串变成整数
#include <stdlib.h>
    int atoi(const char *nptr);
    long atol(const char *nptr);
    long long atoll(const char *nptr);

	   
uint32_t htonl(uint32_t hostlong);
// htonl:把一个32位的数字(主机字节序)转换为网络字节序的数字
	   
uint16_t htons(uint16_t hostshort); <----- 端口号
// htons:把一个16位的数字(主机字节序)转换为网络字节序的数字

uint32_t ntohl(uint32_t netlong);
// ntohl:把一个32位的数字(网络字节序)转换为主机字节序的数字

uint16_t ntohs(uint16_t netshort);
// ntohs:把一个16位的数字(网络字节序)转换为主机字节序的数字
(4) listen:让套接字进入"监听模式"
cpp 复制代码
NAME
    listen - listen for connections on a socket
SYNOPSIS
    #include <sys/types.h>
    #include <sys/socket.h>
		
开启对一个套接字描述符的监听
       
int listen(int sockfd, int backlog);		
	
sockfd:开启对哪一个套接字的监听
		
backlog:监听队列上面最大的请求数量
			
返回值:
	成功返回0
	失败返回-1,同时errno被设置
(5) accept:接收一个监听队列上面的请求
cpp 复制代码
NAME
    accept, accept4 - accept a connection on a socket
SYNOPSIS
    #include <sys/types.h>
    #include <sys/socket.h>
		
accept接收一个套接字监听队列上面的请求
       
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
		
sockfd:你要监听的套接字
		
addr:网络地址结构体指针,指向一块可用的空间,用来保存客户端的地址的
		
addrlen:指针,指向的数据保存第二个参数指针指向的可用空间的长度,防止内存越界,能够把客户
         端的网络地址保存起来,在调用的时候,addrlen一般保存addr指向的那个结构体的大小,
         函数返回时,addrlen指向的变量保存的是客户端地址的实际大小

返回值:
	成功返回一个连接套接字
	表示与一个特定的客户端的连接,后续与这个客户端的数据通信都需要通过这个连接套接字
	
    失败返回-1,同时errno被设置
	阻塞到客户端连接			
(6) connect:主要用于TCP Client 去连接TCP Server
cpp 复制代码
			
NAME
    connect - initiate a connection on a socket
SYNOPSIS
    #include <sys/types.h>
    #include <sys/socket.h>
		
connect用来将参数sockfd表示的soctet文件描述符连接到参数addr描述的网络地址上面去
		
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);		
		
sockfd:本地的套接字描述符
		
addr:指定服务器的地址(ip+端口号),表示你要连接哪一个服务器,是一个网络地址指针
		
addrlen:指定第二个参数的长度,通过第二个参数指针去访问指定的位置,但是不能越界
			
返回值:
    成功返回0
	失败返回-1,同时errno被设置
(7) 发送数据 write / send / sendto

write/send/sendto 这三个函数,TCP都可以使用,但是UDP只能使用sendto

cpp 复制代码
NAME
    send, sendto- send a message on a socket
	// 发送一个数据到指定的socket
SYNOPSIS
    #include <sys/types.h>
    #include <sys/socket.h>
		
send用来往一个套接字上面发送数据

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
		
sockfd:你要向哪一个套接字上面发送数据
		
buf:你要发送的数据的指针
		
len:你要发送多少数据(字节)
		
flags:指定发送标志,一般为0
       0               阻塞模式
       MSG_DONTWAIT    非阻塞模式
		
返回值:
	成功返回实际发送的字节数
	失败返回-1,同时errno被设置
cpp 复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                           const struct sockaddr *dest_addr, socklen_t addrlen);

sendto和send类似,多了两个参数,多的两个参数是指定接收方的地址

TCP是面向连接的通信,可以不指定,因为在通信前已经connect了,sockfd就是一个连接套接字,
已经保存了接收方的地址。但是UDP一定要指定,因为UDP无连接的通信
	   
dest_addr:指定接收方的地址
	   
addrlen:指定接收方的地址的长度
	   
返回值:
	成功返回实际发送的字节数
	失败返回-1,同时errno被设置
(8) 接收数据 read / recv / recvfrom
cpp 复制代码
NAME
    recv, recvfrom -  receive a message from a socket
SYNOPSIS
    #include <sys/types.h>
    #include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
		
recv前面三个参数和read类似,都是从指定的文件描述符中读取count个字节存到buf指向的空间
		
flags:指定接收标志,一般为0
       0               阻塞模式
       MSG_DONTWAIT    非阻塞模式
		
返回值:
    成功返回实际读取的字节数
	失败返回-1,同时errno被设置
cpp 复制代码
recvfrom前面的四个参数与recv一样
  
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, 
                                struct sockaddr *src_addr, socklen_t *addrlen);
		
src_addr:用来保存发送方的地址,TCP/UDP都可以不指定
          TCP一定知道数据的来源的地址,保存在sockfd中
          但是如果UDP不指定,虽然可以收到数据,但是不知道是谁发送给你的
		
addrlen:用来保存发送方地址的长度,也是一个指针
	 	 addrlen一般保存的是src_addr指向的结构体的大小,但是函数返回的时候
         addrlen指向的变量保存的是发送方地址的实际大小
		
返回值:
    成功返回实际读取的字节数
    失败返回-1,同时errno被设置
(9) 关闭套接字 close / shutdown
cpp 复制代码
NAME
    shutdown - shut  down socket send and receive operations
SYNOPSIS
    #include <sys/socket.h>

int shutdown(int socket, int how);	
		
socket:要关闭的套接字
		
how表示关闭方式:
	SHUT_RD  关闭读
	SHUT_WR  关闭写
	SHUT_RDWR 关闭读写--->close
		
返回值:
	成功返回0
	失败返回-1,同时errno被设置

4. TCP服务端和客户端代码实现

tcp_client.c

cpp 复制代码
// 客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("input error");
        return -1;
    }
    // (1) socket:创建一个套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket failed");
        return -1;
    }

    // (2) connect 主动与server建立连接:需要知道服务器的地址
    // 先要准备服务器的网络地址结构体
    struct sockaddr_in sAddr;
    // 设置地址结构体
    memset(&sAddr, 0, sizeof(sAddr));
    // 设置协议族
    sAddr.sin_family = AF_INET;
    // 设置端口号,必须是网络字节序
    sAddr.sin_port = htons(atoi(argv[2]));
    // 设置IP地址
    sAddr.sin_addr.s_addr = inet_addr(argv[1]);

    int ret = connect(sockfd, (struct sockaddr *)(&sAddr), sizeof(sAddr));
    if (ret == -1) {
        perror("connect failed");
        close(sockfd);
        return -1;
    }

    printf("connect success\n");

    while (1) {
    // (3) 读写数据
        char buf[50];
        scanf("%s", buf);
        write(sockfd, buf, sizeof(buf));

        char buff[50] = {0};
        int r = read(sockfd, buff, 50);
        printf("r = %d, buff = %s\n", r, buff);
    }

    // (4) 关闭socket套接字:"四次挥手"
    close(sockfd);

    return 0;
}

tcp_server.c

cpp 复制代码
// 服务端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("input error");
        return -1;
    }
    // (1) socket:创建一个套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket failed");
        return -1;
    }
    // (2) bind
    struct sockaddr_in sAddr;
    memset(&sAddr, 0, sizeof(sAddr));
    sAddr.sin_family = AF_INET;
    sAddr.sin_port = htons(atoi(argv[2]));
    sAddr.sin_addr.s_addr = inet_addr(argv[1]);
    int ret = bind(sockfd, (struct sockaddr *)(&sAddr), (socklen_t)sizeof(sAddr));
    if (ret == -1) {
        perror("bind failed");
        close(sockfd);
        return -1;
    }

    // (3) listen:让套接字进入"监听模式"
    listen(sockfd, 2);

    struct sockaddr_in cAddr;
    socklen_t addrlen = sizeof(cAddr);

    while (1) {
         // (4) accept
        int confd = accept(sockfd, (struct sockaddr *)(&cAddr), &addrlen);
        if (confd != -1) {
			printf("accept  success\n");
			printf("client IP:%s, client port:%d\n", inet_ntoa(cAddr.sin_addr), ntohs(cAddr.sin_port));
			
            // (5) 读写数据
			char buf[256] = {0};
			int r = read(confd, buf, 256);
			if (r > 0) {
				printf("r = %d,message:%s\n", r, buf);
			}
			write(confd, "goodbye", sizeof("goodbye"));
			// 关闭连接
			close(confd);
		}
	}

    // (6) 关闭socket套接字:"四次挥手"
    close(sockfd);

    return 0;
}

5. UDP网络应用

UDP是传输层的一个协议,面向无连接,数据报的传输层协议

无连接:不需要三次握手,数据不可靠
在网络环境比较好的情况下,UDP的传输效率比较高
常用于"实时应用"的情况
数据包具有时效性
在应用层添加一些私有控制协议,提高数据传输的可靠性
编程流程:

发送必须使用sendto,因为数据发送前没有连接,告诉socket,要把数据发给谁

接收数据一般使用recvfrom,也可以使用read / recv,但是这两个函数只能读取到用户数据,不能获取发送方的网络地址

如果客户端没有绑定,那么服务器就不能获取发送方的网络地址

6. UDP服务端和客户端代码实现

相关推荐
豆沙沙包?1 小时前
8.学习笔记-Maven进阶(P82-P89)
笔记·学习·maven
刘婉晴8 小时前
【信息安全工程师备考笔记】第三章 密码学基本理论
笔记·安全·密码学
晓数10 小时前
【硬核干货】JetBrains AI Assistant 干货笔记
人工智能·笔记·jetbrains·ai assistant
我的golang之路果然有问题10 小时前
速成GO访问sql,个人笔记
经验分享·笔记·后端·sql·golang·go·database
lwewan10 小时前
26考研——存储系统(3)
c语言·笔记·考研
搞机小能手11 小时前
六个能够白嫖学习资料的网站
笔记·学习·分类
nongcunqq11 小时前
爬虫练习 js 逆向
笔记·爬虫
汐汐咯11 小时前
终端运行java出现???
笔记
无敌小茶13 小时前
Linux学习笔记之环境变量
linux·笔记
帅云毅14 小时前
Web3.0的认知补充(去中心化)
笔记·学习·web3·去中心化·区块链