socket编程常用函数笔记

  • Post author:
  • Post category:其他

一、什么是Socket

  1. Socket可以看成是用户进程与内核网络协议栈的编程接口
  2. Socket不仅可以用于本机的进程通信,还可以用于网络上不同主机的进程间通信

二、套接字地址结构

  1. IPv4地址结构,以 “sockaddr_in”命名,定义在头文件<netinet/in.h>中
typedef uint32_t in_addr_t;

struct in_addr {
  in_addr_t s_addr;  
};

struct sockaddr_in {
    uint8_t          sin_len;//无符号8位整数,1B
    sa_family_t      sin_family;//1B
    in_port_t        sin_port; // 端口号,无符号16位整数,0~65535,2B
    struct in_addr   sin_addr;//网络地址,4B
    char             sin_zero[8];//8B
};
  • sin_len:整个sockaddr_in结构体的长度
  • sin_family:指定地址家族,IPv4中是AF_INET
  • sin_port:端口
  • sin_addr:IPv4的地址
  • sin_zero:一般设为0

填信息的时候一律用sockaddr_in ,传入API的时候用sockaddr

  1. 通用地址结构
struct sockaddr{
	uint8_t			sa_len;//无符号8位整数,1B
	sa_family_t		sa_family;//1B
	char			sa_data[14];//14B
};
  • 头文件:<sys/socket.h>
  • sin_len:整个sockaddr_in结构体的长度
  • sin_family:指定地址家族,IPv4中是AF_INET,IPv6中是AF_INET6
  • sa_data:由sin_family决定大小

三、套接字函数使用

  1. 字节序转换函数
  • h表示host
  • n表示network
  • l表示long
  • s表示short
uint32_t htonl(uint32_t host);//4个字节整数,由主机字节序转换为网络字节序,网络字节序为大端字节序
uint16_t htons(uint16_t host);//2个字节整数,由主机字节序转换为网络字节序
uint32_t ntohl(uint32_t net);//4个字节整数,由网络字节序准换为主机字节序
uint16_t ntohs(uint16_t net);//2个字节整数,由网络字节序准换为主机字节序
#include<stdio.h>
#include<arpa/inet.h>
/**
 * 验证X86平台为小端字节序
 * 网络字节序为大端字节序
 */
int main(void){
    unsigned int x = 0x12345678;
    unsigned char* p_host = (unsigned char*)&x;
    printf("%0x %0x %0x %0x\n", p_host[0],p_host[1],p_host[2],p_host[3]);//78 56 34 12,小端字节序
    unsigned int y = htonl(x);
    unsigned char *p_net = (unsigned char *)&y;
    printf("%0x %0x %0x %0x\n", p_net[0],p_net[1],p_net[2],p_net[3]);//12 34 56 78
    return 0;
}
  1. 地址转换函数
int inet_aton(const char* cp, struct in_addr* inp);//点分十进制的ip地址转换成网络字节序的ip地址
in_addr_t inet_addr(const char* cp);//点分十进制的ip地址转换成32位整数
char* inet_ntoa(struct in_addr in);//32位整数转换成点分十进制的ip地址
#include<stdio.h>
#include<arpa/inet.h>

int main(void){
    unsigned long addr = inet_addr("192.168.0.100");
    //先将网络字节序转换为主机字节序,再输出
    printf("addr=%u\n", ntohl(addr));//addr=3232235620
    struct in_addr ipAddr;
    ipAddr.s_addr = addr;
    printf("%s\n", inet_ntoa(ipAddr));//192.168.0.100
    return 0;
}

四、套接字类型

  1. 流式套接字(SOCK_STREAM)
  • 提供面向连接的、可靠的数据传输服务,数据无差错,无重复发送,且按序接收(TCP)
  1. 数据报式套接字(SOCK_DGRAM)
  • 提供无连接服务,数据可能丢失或重复,可能顺序混乱(UDP)
  1. 原始套接字(SOCK_RAW)

五、socket编程

1、服务器端函数
  1. 创建套接字
  • 原型:int socket(int domain, int type, int protocol)
  • domain:指定通信协议族,IPv4(AF_INET),IPv6(AF_INET6)
  • type:指定socket类型,流式套接字(SOCK_STREAM)、数据报式套接字(SOCK_DGRAM)、原始套接字(SOCK_RAW)
  • protocol:协议类型,一般写成0
  • 返回值:一般称之为套接字描述字(类似于文件描述符),成功返回非负整数,失败返回-1
int sock = socket(AF_INET, SOCK_STREAM, 0);//套接口描述字
if ( sock < 0) {
    printf("socket error");
    return -1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(7777);//需要写网络字节的端口号
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//若成功,返回32位二进制的网络字节序地址;若有错,则返回 INADDR-NONE 
//inet_aton("127.0.0.1", &server_addr.sin_addr); //点分十进制改成二进制
  1. 绑定函数(数据从进程到内核,服务器)
  • 功能:绑定一个本地地址到套接字
  • 原型:int bind(int sockfd, const struct sockaddr addr, socklen_t addrlen)*
  • sockfd:socket函数返回的套接字
  • addr:要绑定的套接字
  • addrlen:套接字地址长度,IPv4为16,IPv6为24
  • 返回值:成功返回0,失败返回-1
int bindfd = bind(listenfd, (struct sockaddr*)&server_addr, sizeof(server_addr));//服务器端绑定端口
if (bindfd < 0) {
    printf("bind error");
    return -1;
}
  1. 监听函数(服务器)
  • 功能:将套接字用于监听进入的连接
  • 原型:int listen(int sockfd, int backlog)
  • sockfd:socket函数返回的套接字
  • backlog:规定内核为此套接字排队的最大连接数
  • 返回值:成功返回0,失败返回-1

listen只是把套接字从主动变为被动,并限制链接数;剩下的问题就是accept的,它会检测的;
listen意思是监听:但是它不是一直在监听,accept才是;

listen函数不会阻塞,它只是相当于把socket的属性更改为被动连接,可以接收其他进程的连接。listen侦听的过程并不是一直阻塞,直到有客户端请求连接才会返回,它只是设置好socket的属性之后就会返回。监听的过程实质由操作系统完成。但是accept会阻塞(也可以设置为非阻塞),如果listen的套接字对应的连接请求队列为空(没有客户端连接请求),accept会一直阻塞等待

if (listen(listenfd, SOMAXCONN) < 0) {
    printf("listen error");
    return -1;
}
  1. 连接函数(服务器)
  • 功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞
  • 原型:int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen)
  • sockfd:服务器套接字
  • addr:将返回对方的套接字地址
  • addrlen:返回对方的套接字地址长度
  • 返回值:成功返回非负描述字conn,失败返回-1
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn = accept(listenfd, (struct sockaddr*) & client_addr, &client_len);
//主动套接字,内核到用户进程,拿数据(拿客户端的相关连接信息)
if (conn < 0) {
    printf("accept error");
    return -1;
}
2、客户端函数
  1. connect函数
int connect(int sockfd, const struct sockaddr* server_addr, socklen_t addrlen)
  • 返回值
    • 0:成功
    • -1:失败
3、其他函数
  1. 地址转换函数
  • int inet_aton(const char* cp, struct in_addr* inp);//点分十进制的ip地址转换成网络字节序的ip地址
  • in_addr_t inet_addr(const char* cp);//点分十进制的ip地址转换成32位整数
  • char* inet_ntoa(struct in_addr in);//32位整数转换成点分十进制的ip地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
inet_aton("127.0.0.1", &server_addr.sin_addr);
  1. 新型网路地址转化函数inet_pton和inet_ntop(to point)
  • 头文件:#include <arpe/inet.h>
  • int inet_pton(int family, const char *strptr, void *addrptr) //将点分十进制的ip地址转化为用于网络传输的数值格式
  • 返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
  • const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len); //将数值格式转化为点分十进制的ip地址格式
  • 返回值:若成功则为指向结构的指针,若出错则为NULL
int main (void)
{
	char IPdotdec[20]; //存放点分十进制IP地址
	struct in_addr s; // IPv4地址结构体
	// 输入IP地址
	printf("Please input IP address: ");
	scanf("%s", IPdotdec);
	// 转换
	inet_pton(AF_INET, IPdotdec, (void *)&s);//点分十进制的ip地址转换为二进制放到s
	printf("inet_pton: 0x%x\n", s.s_addr); // 注意得到的字节序
	// 反转换
	inet_ntop(AF_INET, (void *)&s, IPdotdec, 16);
	printf("inet_ntop: %s\n", IPdotdec);
}
inet_pton(AF_INET, IPdotdec, &src.sin_addr);//src.sin_addr.s_addr = inet_addr(IPdotdec);

char IPdotdec[INET_ADDRATRLEN];
ptr = inet_ntop(AF_INET, &src.sin_addr, IPdotdec, sizeof(IPdotdec));//ptr = inet_ntoa(src.sin_addr);
  1. readn函数
  • *ssize_t readn(int fd, void vptr, size_t n)
  • 功能:从描述字中读取n个字节为止或读到EOF为止
  • 返回值:

1.大于0,代表成功读取的字节数

2.等于0,代表读取到了EOF,一般是对方关闭了socket的写端或者直接close

3.小于0,出现错误。

ssize_t	readn(int fd, void *vptr, size_t n)
{
	size_t	nleft;//剩余读取字节数
	ssize_t	nread;//已经读取的字节数
	char	*ptr;

	ptr = vptr;
	nleft = n;//总共需要读取的字节数
	while (nleft > 0) {
		if ( (nread = read(fd, ptr, nleft)) < 0) {
			if (errno == EINTR)
				nread = 0;		/* and call read() again */
			else
				return(-1);
		} else if (nread == 0)
			break;				/* 读到了EOF */

     //正确从描述字读取到nread字节的数据
		nleft -= nread;
		ptr += nread;
	}
	return(n - nleft);		/* return >= 0 */
}

ssize_t Readn(int fd, void *ptr, size_t nbytes)
{
	ssize_t n;
	if ( (n = readn(fd, ptr, nbytes)) < 0)
		err_sys("readn error");
	return(n);
}
  1. writen函数
  • 原型:*ssize_t writen(int fd, const void vptr, size_t n)
  • 功能:往描述字中写入n个字节
  • 返回值

小于0:错误

大于0:成功写入n个字节

/* Write "n" bytes to a descriptor. */

ssize_t writen(int fd, const void *vptr, size_t n)
{
	size_t		nleft;
	ssize_t		nwritten;
	const char	*ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0) {
		if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
			if (errno == EINTR)
				nwritten = 0;		/* and call write() again */
			else
				return(-1);			/* error */
		}

		nleft -= nwritten;
		ptr   += nwritten;
	}
	return(n);
}
/* end writen */

void Writen(int fd, void *ptr, size_t nbytes)
{
	if (writen(fd, ptr, nbytes) != nbytes)
		err_sys("writen error");
}
  1. readline函数
  • 原型:ssize_t readline(int fd, void *vptr, size_t maxlen)
  • 功能:从描述字中一个字节一个字节地读取放到缓冲区vptr,直到读到最大长度maxlen,或读到换行符,或读完数据
  • 返回值

大于0:返回的是成功读取到的字节数

等于0:没有数据读取

小于0:错误


ssize_t readline(int fd, void *vptr, size_t maxlen)
{
	ssize_t	n, rc;
	char	c, *ptr;

	ptr = vptr;
	for (n = 1; n < maxlen; n++) {
again:
		if ( (rc = read(fd, &c, 1)) == 1) {
			*ptr++ = c;
			if (c == '\n')
				break;	/* newline is stored, like fgets() */
		} else if (rc == 0) {
			if (n == 1)
				return(0);	/* EOF, no data read */
			else
				break;		/* EOF, some data was read */
		} else {
			if (errno == EINTR)
				goto again;
			return(-1);		/* error, errno set by read() */
		}
	}

	*ptr = 0;	/* null terminate like fgets() */
	return(n);
}
/* end readline */

ssize_t Readline(int fd, void *ptr, size_t maxlen)
{
	ssize_t n;

	if ( (n = readline(fd, ptr, maxlen)) < 0)
		err_sys("readline error");
	return(n);
}
  1. read函数
  • 原型:ssize_t write(int fd, const void*buf,size_t nbytes);

  • 功能:将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数.失败时返回-1. 并设置errno变量

  • 返回值

大于0,表示写了部分或者是全部的数据

小于0,此时出现了错误

  1. write函数
  • 原型:*ssize_t read(int fd,void buf,size_t nbyte)

  • 功能:read函数是负责从fd中读取内容

  • 返回值

大于0,返回的是实际所读的字节数

小于0,表示出现了错误

socket套接字是全双工的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、完整的tcp server代码

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

int main(){
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1){
        printf("create socket failed!\n");
        return 0;
    }
    // 两个套接字地址
    struct sockaddr_in ser_addr, cli_addr;
    memset(&ser_addr, 0, sizeof(ser_addr));
    
    ser_addr.sin_family = AF_INET;    // 地址族
    ser_addr.sin_port = htons(8888);  // host to net short
    ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // inet_addr:字符串转成整型的网络字节序点分十进制的ip地址

    // sockfd就是参观门口的服务员   ser_addr就是餐厅的地址     bind表示sockfd服务员为sockaddr餐厅工作
    // 给监听套接字指定ip port
    // sockfd表示手机    ser_addr是手机卡    ser_addr是卡大小   bind表示给手机插卡
    int res = bind(sockfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr)); // struct sockaddr* 通用套接字地址结构
    if(res == -1){
        printf("bind failed!\n");
        return 0;
    }
    // 套接字 监听队列的大小
    listen(sockfd, 5);    // 创建监听队列(已完成三次握手的连接长度) 
    while(1){
        int len = sizeof(cli_addr);
        // accept从监听队列中取出连接,将信息取出来存入cli_addr。conn是文件描述符,就是餐馆内点菜的服务员
        int conn = accept(sockfd, (struct sockaddr*)&cli_addr, &len); 
        if(conn < 0){
            // 取出连接失败
            continue;
        }
        printf("accept conn = %d\n", conn);
        while(1){
            char buff[128] = {0};
            // ssize_t recv(int sockfd, void *buf, size_t len, int flags);
            int n = recv(conn, buff, 127, 0); // client断开连接,就会返回0
            if(n == 0){
                printf("client %d exit\n", conn);
                break;
            }
            printf("buff(%d) = %s\n", n, buff);
            // ssize_t send(int sockfd, const void *buf, size_t len, int flags);
            send(conn, "welcome connect server!\n", sizeof("welcome connect server!\n"), 0);
        }
        close(conn);
    }
    return 0;
}

七、完整的tcp client代码

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>

int main(){
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1){
        printf("create socket failed!\n");
        return 0;
    }
    struct sockaddr_in cli_addr;
    memset(&cli_addr, 0, sizeof(cli_addr));

    cli_addr.sin_family = AF_INET;    // 地址族
    cli_addr.sin_port = htons(8888);  // host to net short
    cli_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // inet_addr将字符串转为无符号整型

    // 可以将套接字绑定ip,但一般客户端不绑定,让OS随机分配port
    int res = connect(sockfd, (struct sockaddr*)&cli_addr, sizeof(cli_addr));  // 连接server
    assert(res != -1);

    while(1){
        char buff[128] = {0};
        printf("input:");
        fgets(buff, 128, stdin);
        if(strcmp(buff, "exit\n") == 0){
            break;
        }
        send(sockfd, buff, strlen(buff), 0);
        memset(buff, 0 ,128);
        int n = recv(sockfd, buff, 127, 0);
        printf("buff(%d) = %s", n, buff);
    }

    close(sockfd);
    return 0;
}

八、完整的多进程tcp client代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>  
#include <sys/wait.h>
#include <arpa/inet.h>

// 开启一个子进程服务客户端cli_addr
void serve_client(int cli_fd,  struct sockaddr* cli_addr){
    pid_t pid = fork();
    if(pid < 0){
        printf("fork error!\n");
        close(cli_fd);
        return ;
    }else if(pid == 0){
        while(1){
            char buff[128] = {0};
            // ssize_t recv(int listen_sock, void *buf, size_t len, int flags);
            int n = recv(cli_fd, buff, 127, 0); // client断开连接,就会返回0
            if(n == 0){
                printf("client %d exit\n", cli_fd);
                break;
            }
            printf("from client %s:%d:\n", 
                    inet_ntoa(((struct sockaddr_in*)cli_addr)->sin_addr), 
                    ntohs(((struct sockaddr_in*)cli_addr)->sin_port));
            printf("buff(%d) = %s\n", n, buff);
            // ssize_t send(int listen_sock, const void *buf, size_t len, int flags);
            send(cli_fd, "ok\n", sizeof("ok\n"), 0);
        }
        close(cli_fd);    // 子进程使用完close
        exit(0);
    }else{
        // 父进程
        close(cli_fd);          // fork后,父子进程都使用同一个cli_fd,cli_fd会有引用计数,父进程不用先close
        waitpid(pid,NULL,WNOHANG|WUNTRACED);    // 告诉kernel,提醒处理zombies
    }
}

int main(){
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock == -1){
        printf("create socket failed!\n");
        return 0;
    }
    // 两个套接字地址
    struct sockaddr_in ser_addr, cli_addr;
    memset(&ser_addr, 0, sizeof(ser_addr));
    
    ser_addr.sin_family = AF_INET;    // 地址族
    ser_addr.sin_port = htons(8888);  // host to net short
    ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // inet_addr将字符串转为无符号整型

    // listen_sock就是参观门口的服务员   ser_addr就是餐厅的地址     bind表示listen_sock服务员为sockaddr餐厅工作
    // 给监听套接字指定ip port
    // listen_sock表示手机    ser_addr是手机卡    ser_addr是卡大小   bind表示给手机插卡
    int res = bind(listen_sock, (struct sockaddr*)&ser_addr, sizeof(ser_addr)); // struct sockaddr* 通用套接字地址结构
    if(res == -1){
        printf("bind failed!\n");
        return 0;
    }
    // 套接字 监听队列的大小
    listen(listen_sock, 1);    // 创建监听队列(已完成三次握手的连接长度) 
    while(1){
        int len = sizeof(cli_addr);
        // accept从监听队列中取出连接,将信息取出来存入cli_addr。client_fd是文件描述符,就是餐馆内点菜的服务员
        int client_fd = accept(listen_sock, (struct sockaddr*)&cli_addr, &len);
        if(client_fd < 0){
            // 取出连接失败
            printf("客户端连接失败\n");
            continue;
        }
        printf("accept client_fd = %d connected\n", client_fd);
        // 在这里服务客户,父进程和子进程都会关闭描述符
        serve_client(client_fd, (struct sockaddr*)&cli_addr);
    }
    return 0;
}

抓包命令: tcpdump -i <网卡> -nt -S '(src dst) or (src dst)'

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

流式服务和粘包问题

在这里插入图片描述

在这里插入图片描述

HTTP协议解决粘包问题: 在每个数据报头写入当前报文的长度,接收端会根据该长度控制或分割自己接收的数据包。

TCP状态转移总图

在这里插入图片描述

TIME_WAIT状态的意义: 主动关闭的一方 收到对方的FIN,自己回复ACK后,并没有直接进入CLOSED状态,而是进入TIME_WAIT状态一段时间(大约是报文最长生存时间的2倍)。

  1. 若先关闭的一方回复ACK后,直接进入CLOSED状态,假如ACK丢失了,后关闭的一方会重发FIN,此时先关闭的一方无法接收,四次挥手无法完成。(若后关闭的一方不重发FIN,则说明成功收到ACK)
  2. 进入TIME_WAIT,若后关闭的一方没收到ACK,会再次发送FIN,进入TIME_WAIT的一方重发ACK。

为什么TIME_WAIT状态大约是报文最长生存时间的2倍?
假设网络拥塞,数据包在网络中传送了很长时间,若发送ACK后没有TIME_WAIT状态直接关闭连接,而这时又收到了迟到的数据包就无法处理。
TIME_WAIT状态大约是报文最长生存时间的2倍,是为了保证能正确收到所有能被送达的数据包。
若先关闭的一方处于TIME_WAIT状态,则程序无法重新启动

九、完整的udp server代码

udp编程流程
在这里插入图片描述

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

int main(){
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    assert(sockfd != -1);

    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0, sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(8080);
    ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
		
	// 指定socket的ip和端口,ip指定主机,端口指定进程
    int res = bind(sockfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
    assert(res != -1);

    while(1){
        // 用于存储客户端的地址
        struct sockaddr_in cli_addr;
        int len = sizeof(cli_addr);
        char buff[128] = {0};
        // 把收到的数据放在buff,ip放在cli_addr
        int n = recvfrom(sockfd, buff, 127, 0, (struct sockaddr*)&cli_addr, &len);
        printf("recv(%d) = %s", n, buff);
        sendto(sockfd, "welcome connect server!\n", 
            sizeof("welcome connect server!\n"),0, 
            (struct sockaddr*)&cli_addr,sizeof(cli_addr));
    }
    return 0;
}

十、完整的udp client代码

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

int main(){
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    assert(sockfd != -1);

    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0, sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(8080);
    ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	
    while(1){
        char buff[128] = {0};
        printf("input:");
        fflush(stdout);

        fgets(buff, 128, stdin);
        if(strcmp(buff, "exit\n") == 0){
            break;
        }
        // 通过sockfd发送buff给ser_addr
        // 服务器需要bind,而客户端不需要bind,是因为客户端的端口默认由系统自行分配
        sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
        int len = sizeof(ser_addr);
        int n = recvfrom(sockfd, buff, 127, 0, (struct sockaddr*)&ser_addr, &len);
        printf("recv(%d) = %s", n, buff);
    }
    close(sockfd);
    return 0;
}

十一、对比TCP字节流服务和UDP数据包服务

TCP服务中将收取的字节改为1

int n = recv(conn, buff, 1, 0);

在这里插入图片描述
UDP服务中将收取的字节改为1

recvfrom(sockfd, buff, 1, 0, (struct sockaddr*)&cli_addr, &len);

在这里插入图片描述

在这里插入图片描述
tcp可以把多次send的数据放到缓冲区然后发送,是因为tcp发送的数据一定是发送给指定的主机。而udp不能合并报文,是因为每个udp数据报去往的主机不一定相同。

对于tcp而言,客户端发送一个报文hello到了tcp缓冲区,虽然进程一次recv只能取一个字节,但只要tcp缓冲区不空,就会不断执行while循环,不断 从缓冲区中取数据
对于udp而言,一次recvfrom直接从数据报中取数据 ,只能取一个字节,由于udp服务没有数据缓冲区,所以一个报文的数据没收完就丢失了。

面试题:如果server和client建立连接后,server的网线断了,然后断电重启联网,重启进程后,server和client分别处于什么状态?

断网断电后,server无法发出任何的报文通知client,所以client认为连接就是正常的,除非client发出报文,才会发现连接异常。服务器重启后,和原来客户端的连接已经丢失,处于监听状态

在这里插入图片描述

十二、一个单线程的HTTP服务器

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

char* get_filename(char msg[]){
	// 例:msg = "GET /index.html HTTP/1.1"
    if(msg == NULL){
        return NULL;
    }
    char* str = strtok(msg, " ");
    if(str == NULL){
        return NULL;
    }
    printf("请求的方法是:%s\n", str);

    str = strtok(NULL, " ");
    if(str == NULL){
        return NULL;
    }
    return str;
}

int socket_init(){
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1){
        return -1;
    }
    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0 ,sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(80);
    ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = bind(sockfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
    assert(res != -1);

    res = listen(sockfd, 5);
    assert(res != -1);

    return sockfd;
}

int main(){
    int sockfd = socket_init();
    while(1){
        struct sockaddr_in cli_addr;
        int len = sizeof(cli_addr);
        int conn = accept(sockfd, (struct sockaddr*)&cli_addr, &len);
        if(conn < 0){
            continue;
        }
        char recv_buff[512] = {0};
        recv(conn, recv_buff, 511, 0); // 接收浏览器发送的数据 
        printf("read:\n%s\n", recv_buff);

        char* filename = get_filename(recv_buff);  // 解析请求路径,拿到访问的文件名
        if(filename == NULL){
            send(conn, "ERROR!", 6, 0);
            close(conn);
            continue;
        }
        char path[128] = {"/home/shen/code"};   // 拼接请求路径
        if(strcmp(filename, "/") == 0){
            strcat(path, "/index.html");
        }else{
            strcat(path, filename);
        }
        printf("-------------------------\n");
        printf("客户请求资源:%s\n", path);
        printf("-------------------------\n");

        int file_fd = open(path, O_RDONLY);
        if(file_fd == -1){
            send(conn, "404", 3, 0);
            close(conn);
            continue;
        }

        int file_size = lseek(file_fd, 0, SEEK_END); // 计算文件的大小
        lseek(file_fd, 0, SEEK_SET);        // 移回指针

        char head_buff[512] = {"HTTP/1.0 200 OK\r\n"};
        strcat(head_buff, "Server: myhttp\r\n");
        // head_buff+strlen(head_buff)就是在head_buff后面追加的意思,直接head_buff就是覆盖了
        sprintf(head_buff+strlen(head_buff), "Content-Length:%d\r\n", file_size); 
        strcat(head_buff, "\r\n");
        send(conn, head_buff, strlen(head_buff), 0);  // 先发头部
        printf("send head:%s\n", head_buff);

        // 发送文件中的数据
        char data_buff[1024] = {0};
        int num = 0;
        while((num = read(file_fd, data_buff, 1024)) > 0){
            send(conn, data_buff, num, 0);
        }
        close(file_fd);
        close(conn);
    }
    return 0;
}

在这里插入图片描述

十三、libevent的使用

libevent就是一个封装好select、poll、epoll等功能的库

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <event.h>
#include <signal.h>

// event_new(base实例, 信号, 事件, 回调函数, 回调函数的参数)
// evsignal_new只是帮助我们固定了信号的类型为 信号事件|永久事件
// evtimer_new没有描述符,参数为-1,没有事件,参数为0
#define evsignal_new(b,x,cb,arg) event_new((b), (x), EV_SIGNAL|EV_PERSIST, (cb), (arg))
#define evtimer_new(b,cb,arg) event_new((b), -1, 0, (cb), (arg))


void signal_cb(int fd, short event, void* argc){
    printf("sig=%d\n", fd);
}

void timeout_cb(int fd, short event, void* argc){
    printf("timeout\n");
}

int main(){
    // libevent实例,分配空间,初始化
    struct event_base* base = event_init();
    assert(NULL != base);

    // 定义事件
    // struct event* signal_event = evsignal_new(base, SIGINT, signal_cb, NULL);
    struct event* signal_event = event_new(base, SIGINT, EV_SIGNAL|EV_PERSIST, signal_cb, NULL);
    assert(NULL != signal_event);
    // 事件添加到实例,无超时时间
    event_add(signal_event, NULL);

    struct timeval tv = {1 ,0};
    // struct event* timeout_event = evtimer_new(base, timeout_cb, NULL);
    struct event* timeout_event = event_new(base, -1, 0, timeout_cb, NULL);
    assert(NULL != timeout_event);
    // 设置超时事件
    event_add(timeout_event, &tv);
    
    // 启动事件循环
    event_base_dispatch(base);

    // 释放malloc分配的资源
    event_free(timeout_event);
    event_free(signal_event);
    event_base_free(base);

    return 0;
}

使用libevent写的tcp服务器

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <event.h>

int socket_init(){
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1){
        return -1;
    }
    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0 ,sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(8888);
    ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = bind(sockfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
    assert(res != -1);

    res = listen(sockfd, 5);
    assert(res != -1);

    return sockfd;
}

// fd描述符发生了ev事件,arg是用户给回调函数recv_cb传入的参数
void recv_cb(int fd, short ev, void* arg){
    if(ev & EV_READ){
        char buff[128] = {0};
        int n = recv(fd, buff, 127, 0);
        if(n <= 0){
            struct event** conn_ev_ptr = (struct event**)arg;
            event_free(*conn_ev_ptr);  // *conn_ev_ptr是event实例
            free(conn_ev_ptr);         // 这是手动malloc的空间
            close(fd);

            printf("client %d close\n", fd);
            return;
        }
        printf("recv : %s\n", buff);
        send(fd, "ok", 2, 0);
        
    }
}

void accept_cb(int fd, short ev, void* arg){
    struct event_base* base = (struct event_base*)arg;
    assert(NULL != base);

    if(ev & EV_READ){
        struct sockaddr_in cli_addr;
        int len = sizeof(cli_addr);
        int conn = accept(fd, (struct sockaddr*)&cli_addr, &len);
        if(conn < 0){
            return ;
        }
        printf("accept conn = %d\n", conn);
        
        // 给客户端建立连接后,用该描述符创建struct event,并加入libevent实例
        // struct event* conn_ev = event_new(base, conn, EV_READ|EV_PERSIST, recv_cb, base);
        struct event** conn_ev_ptr = (struct event**)malloc(sizeof(struct event*));
        assert(NULL != conn_ev_ptr);
        // 用libevent实例、连接和事件构造一个event,最后把这个event添加到libevent实例
        // 首先用构造的event实例给*conn_ev_ptr赋值,当有读事件发生的时候,会把conn_ev_ptr传入recv_cb处理读事件
        *conn_ev_ptr = event_new(base, conn, EV_READ|EV_PERSIST, recv_cb, conn_ev_ptr);
        assert(NULL != *conn_ev_ptr);

        event_add(*conn_ev_ptr, NULL);

    }
}

int main(){
    // libevent实例,分配空间,初始化
    struct event_base* base = event_init();
    assert(NULL != base);

    int sockfd = socket_init();
    assert(sockfd != -1);

    struct event* sock_ev = event_new(base, sockfd, EV_READ|EV_PERSIST, accept_cb, base);
    // 监听描述符添加到libevent
    event_add(sock_ev, NULL);

    event_base_dispatch(base);
    event_free(sock_ev);
    event_base_free(base);

    return 0;
}

版权声明:本文为qq_42500831原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。