使用socket实现简单的TCP和UDP通信模型

预备知识

socket基本概念

一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。)

在通信过程中, 套接字一定是成对出现的。

一个TCP连接的套接字对是一个定义该连接的两个端点的四元组:源IP地址、源TCP端口号、目标IP地址、目标TCP端口号。套接字对唯一标识一个网络上的每个TCP连接。

标识每个端点的两个值(IP地址和端口号)通常称为一个套接字。

套接字=IP地址+端口号

网络字节序

网络字节序是大端存储方式,高位数据存在低地址,低位数据存储在高地址。而本地PC一般都是小端存储,因此在处理IP地址和端口号时,需要进行网络字节序和本地字节序之间的转换。

相关函数

网络字节序和本地字节序转换函数

htons函数-本地端口号转换为网络字节序

uint16_t htons(uint16_t hostshort);

ntohs函数-网络字节序端口号转换为本地端口号

uint16_t ntohs(uint16_t netshort);

inet_pton函数-本地IP地址转换为网络字节序

int inet_pton(int af, const char *src, void *dst);

参数:

  • af:AF_INET、AF_INET6
  • src:传入,IP地址(点分十进制)如127.0.0.1
  • dst:传出,转换后的 网络字节序的 IP地址。

返回值:

  • 成功: 1
  • 异常: 0, 说明src指向的不是一个有效的ip地址。
  • 失败:-1

inet_ntop函数-网络字节序IP地址转换为本地IP地址

const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);

参数:

  • af:AF_INET、AF_INET6
  • src: 网络字节序IP地址
  • dst:本地字节序(string IP)
  • size: dst 的大小。

返回值:

  • 成功:dst。
  • 失败:NULL

sockaddr_in地址结构体

IP + port --> 在网络环境中唯一标识一个进程

struct sockaddr_in addr; //定义地址结构体

addr.sin_family = AF_INET/AF_INET6; //设置协议族,IPv4或者IPv6
addr.sin_port = htons(9527); //设置端口号
addr.sin_addr.s_addr = htonl(INADDR_ANY); //取出系统中有效的任意IP地址。二进制类型。
//或者
/*
int dst;
inet_pton(AF_INET, "192.157.22.45", (void *)&dst);
addr.sin_addr.s_addr = dst;
*/

CS模型通信相关函数

socket函数-创建套接字

#include <sys/socket.h>
int socket(int domain, int type, int protocol);		//创建一个 套接字

参数:

  • domain:AF_INET、AF_INET6、AF_UNIX(域套接字,可用于不同主机进程间通信)
  • type:SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)
  • protocol: 0(表示只用domain项指定的协议族)、非0(自己指定协议族)

返回值:

  • 成功: 新套接字所对应文件描述符
  • 失败: -1 errno

bind函数-为套接字绑定IP地址和端口号

 #include <arpa/inet.h>
 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);		//给socket绑定一个 地址结构 (IP+port)

参数:

  • sockfd: socket 函数返回值

  • struct sockaddr_in addr; //传入参数(struct sockaddr *)&addr强制类型转换

    • addr.sin_family = AF_INET;
    • addr.sin_port = htons(8888);
    • addr.sin_addr.s_addr = htonl(INADDR_ANY);
  • addrlen: sizeof(addr) 地址结构的大小。

返回值:

  • 成功:0
  • 失败:-1 errno

listen函数-设置套接字的监听上限

int listen(int sockfd, int backlog);		//设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)

参数:

  • sockfd: socket 函数返回值
  • backlog:上限数值。最大值 128.

返回值:

  • 成功:0
  • 失败:-1 errno

accept函数-阻塞监听其他套接字的连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);	//阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。

参数:

  • sockfd: socket 函数返回值
  • addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)
  • addrlen:传入传出。 &clit_addr_len
    • socklen_t clit_addr_len = sizeof(addr);
    • 入:addr的大小。 出:客户端addr实际大小。

返回值:

  • 成功:能与客户端进行数据通信的 socket 对应的文件描述。
  • 失败: -1 , errno

connect函数-向指定IP地址端口号的套接字发起连接请求

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);	  //使用现有的 socket 与服务器建立连接

参数:

  • sockfd: socket 函数返回值
  • struct sockaddr_in srv_addr; // 传入参数,服务器地址结构
    • srv_addr.sin_family = AF_INET;
    • srv_addr.sin_port = 9527; //跟服务器bind时设定的 port 完全一致。
    • inet_pton(AF_INET, “服务器的IP地址”,&srv_adrr.sin_addr.s_addr);
  • addrlen://服务器的地址结构的大小

返回值:

    • 成功:0
    • 失败:-1 errno

如果不使用bind绑定客户端地址结构, 采用"隐式绑定".

套接字IO函数

read函数-从套接字读数据

ssize_t read(int fd, void *buf, size_t count);

参数:

  • fd:文件描述符
  • buf:存数据的缓冲区
  • count:缓冲区大小

返回值:

  • 0:表示文件读到末尾
  • 成功:实际读到的字节数
  • 失败:-1,设置errno
  • -1:并且errno=EAGAIN或EWOULDBLOCK,说明不是read失败,而是read在以非阻塞方式读一个阻塞文件(设备文件、网络文件),并且文件无数据。

write函数-向套接字写数据

ssize_t write(int fd, const void *buf, size_t count);

参数:

  • fd:文件描述符
  • buf:待写出的数据缓冲区
  • count:数据的大小

返回值:

  • 成功:写入的字节数
  • 失败:-1,设置errno

recv函数-TCP专用,从套接字读数据

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数:

  • sockfd:指定接收端套接字描述符
  • buf:指向一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
  • len:指明buf的长度;
  • flags:一般置为0;

返回值:

  • 成功:返回值是返回接收数据的长度。
  • 超时或者对端主动关闭:0
  • 失败:<0,并设置errno

send函数-TCP专用,向套接字写数据

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数:

  • sockfd:指定发送端套接字描述符;
  • buf:指明一个存放应用程序要发送数据的缓冲区;
  • len:指明实际要发送的数据的字节数;
  • flags:一般置0;

返回值:

  • 成功:已发送的数据的长度
  • 超时或者对端主动关闭:0
  • 失败:<0,并设置errno

recvfrom-UDP专用,从套接字读数据

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

参数:

  • sockfd: 套接字

  • buf:缓冲区地址

  • len:缓冲区大小

  • flags: 0

  • address:传出参数

  • address_len:传入传出参数

返回值:

  • 成功:成功接收数据字节数
  • 失败:-1,设置errno

sendto函数-UDP专用,向套接字写数据

#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

参数:

  • sockfd: 套接字
  • buf:存储数据的缓冲区
  • len:数据长度
  • flags: 0
  • dest_addr:传入。 目标地址结构
  • dest_len:地址结构长度

返回值:

  • 成功写出数据字节数
  • 失败:-1,设置errno

C/S模型-TCP

通过CS模型,客户端发送小写的字符串给服务器端,服务器端实现小写转大写,并返回给客户端转换后的字符串。

server.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/socket.h>
#include<ctype.h>
#include<arpa/inet.h>

//宏定义服务器的端口号
#define SERV_PORT 9527

int main(int argc,char** argv)
{
	//定义数据缓冲区,用于接受来自客户端的数据
	char buf[BUFSIZ]; //BUFSIZ为4096
	int length=1; //实际接受的长度

	//定义监听的文件描述符
	int lfd=0;

	//定义接收到的客户端的IP地址
	char client_IP[1024];

	//定义地址结构体
	struct sockaddr_in serv_addr;
	//初始化地址结构,添加IP地址和port地址
	serv_addr.sin_family=AF_INET; //IPv4
	serv_addr.sin_port = htons(SERV_PORT); //端口号,此处需要将本地端口号转换成端口号对应的网络序列号
	serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);//本地IP地址转换为对应的网络序列号

	//【1】创建socket对象
	lfd = socket(AF_INET,SOCK_STREAM,0); //使用IPv4和TCP进行网络通信
	if(lfd == -1)
	{
		perror("socket errno");
		exit(1);
	}

	//【2】使用已定义初始化好的地址结构绑定IP地址和端口号
	//参数:文件描述符、地址结构体(struct sockaddr_in*转换成struct sock_addr*)、结构体的长度
	int ret = bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
	if(ret == -1)
	{
		perror("bind errno");
		exit(1);
	}

	//【3】设置监听上限
	ret = listen(lfd,10);
	if(ret == -1)
	{
		perror("listen errno");
		exit(1);
	}

	//【4】阻塞等待客户端的连接请求
	//定义监听得到的客户端的地址结构和地址结构的长度变量
	struct sockaddr_in clit_addr=serv_addr;
	socklen_t clit_len = sizeof(clit_addr);
	//定义实际用户和客户端进行通信的文件描述符
	int sfd = accept(lfd,(struct sockaddr*)&clit_addr,&clit_len);
	if(sfd == -1)
	{
		perror("accept errno");
		exit(1);
	}

	//打印客户端的IP地址和端口号
	printf("client ip:%s port:%d\n",
			inet_ntop(AF_INET,&clit_addr.sin_addr.s_addr,client_IP,1024),
			ntohs(clit_addr.sin_port));

	//【5】通过sfd文件描述符对接收到的信息进行处理
	int i;
	int j=0;
	while(length != 0)
	{
		length = read(sfd,buf,sizeof(buf));
		//此时缓冲区buf中,包含了实际长度为length的数据
		//打印接受来自客户端的数据
		printf("[%d] Get Message from client:\n",j);
		write(STDOUT_FILENO,buf,length);
		//数据处理
		for(i=0;i<length;i++)
		{
			buf[i]=toupper(buf[i]);
		}
		printf("[%d]After Modify:\n",j);
		write(STDOUT_FILENO,buf,length);
		printf("=======\n");
		//将数据返回到客户端
		write(sfd,buf,length);
		j++;
	}


	//【6】关闭文件描述符
	close(lfd);
	close(sfd);
	return 0;
}

client.c

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

#define SERV_PORT 9527

int main(int argc,char **argv)
{
	char buf[1024];
	int length;
	//为了建立与服务器端的连接,客户端需要知道服务器的IP地址和端口号
	//初始化需要链接的服务器的地址结构
	struct sockaddr_in ser_addr;
	ser_addr.sin_family=AF_INET;
	ser_addr.sin_port=htons(SERV_PORT);
	//需要将服务器端的IP地址进行转换
	int ret = inet_pton(AF_INET,"127.0.0.1",&ser_addr.sin_addr.s_addr);
	if(ret == -1)
	{
		perror("inet_pton error");
		exit(1);
	}

	//【1】创建套接字
	int cfd = socket(AF_INET,SOCK_STREAM,0);
	if(cfd == -1)
	{
		perror("socket errno");
		exit(1);
	}
	
	//【2】connect函数与目标服务器建立连接
	//需要强制类型换换0
	//sockaddr_in *转换为sockaddr *
	ret = connect(cfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));
	if(ret == -1)
	{
		perror("connect error");
		exit(1);
	}

	//【3】向文件描述符cfd进行写操作,数据会传输到服务器
	int i = 0;
	while(i<10)
	{
		write(cfd,"i love you\n",12);
		length = read(cfd,buf,1024);
		write(STDOUT_FILENO,buf,length);
		i++;
	}
	close(cfd);
	
	return 0;
}

C/S模型-UDP

通过CS模型,客户端发送小写的字符串给服务器端,服务器端实现小写转大写,并返回给客户端转换后的字符串。
server.c

#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>

#define SERV_PORT 8000

int main(void)
{
    struct sockaddr_in serv_addr, clie_addr;
    socklen_t clie_addr_len;
    int sockfd;
    char buf[BUFSIZ];
    char str[INET_ADDRSTRLEN];
    int i, n;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(SERV_PORT);

    bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    printf("Accepting connections ...\n");
    while (1) {
        clie_addr_len = sizeof(clie_addr);
        n = recvfrom(sockfd, buf, BUFSIZ,0, (struct sockaddr *)&clie_addr, &clie_addr_len);
        if (n == -1)
            perror("recvfrom error");

        printf("received from %s at PORT %d\n",
                inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
                ntohs(clie_addr.sin_port));

        for (i = 0; i < n; i++)
            buf[i] = toupper(buf[i]);

        n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&clie_addr, sizeof(clie_addr));
        if (n == -1)
            perror("sendto error");
    }

    close(sockfd);

    return 0;
}

client.c

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>

#define SERV_PORT 8000

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    int sockfd, n;
    char buf[BUFSIZ];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    while (fgets(buf, BUFSIZ, stdin) != NULL) {
        n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
        if (n == -1)
            perror("sendto error");

        n = recvfrom(sockfd, buf, BUFSIZ, 0, NULL, 0);         //NULL:不关心对端信息
        if (n == -1)
            perror("recvfrom error");

        write(STDOUT_FILENO, buf, n);
    }
    close(sockfd);
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值