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 小时前
SpringMVC学习笔记(二)
笔记·学习
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
huangkj-henan7 小时前
DA217应用笔记
笔记
Young_202202027 小时前
学习笔记——KMP
笔记·学习
秀儿还能再秀7 小时前
机器学习——简单线性回归、逻辑回归
笔记·python·学习·机器学习
WCF向光而行7 小时前
Getting accurate time estimates from your tea(从您的团队获得准确的时间估计)
笔记·学习
Li_0304069 小时前
Java第十四天(实训学习整理资料(十三)Java网络编程)
java·网络·笔记·学习·计算机网络
啤酒泡泡_Lyla9 小时前
现代无线通信接收机架构:超外差、零中频与低中频的比较分析
笔记·信息与通信
龙中舞王10 小时前
Unity学习笔记(4):人物和基本组件
笔记·学习·unity
红色的山茶花10 小时前
YOLOv7-0.1部分代码阅读笔记-general.py
笔记·yolo