TCP通信实现客户端向服务器发送图片

TCP通信:

1. TCP 协议通信交互流程:
具体的流程如下:
(1)服务器根据地址类型(ipv4、ipv6)、socket 类型、协议创建 socket.
(2)服务器为 socket 绑定 ip 地址和端口号。
(3)服务器 socket 监听端口号的请求,随时准备接受来自客户端的连接,此时服务器的 socket
处于关闭状态。
(4)客户端创建 socket。
(5)客户端打开 socket,根据服务器 ip 地址和端口号试图与服务器的 socket 建立连接。
(6)服务器的 socket 接收到来自客户端的 socket 连接请求之后,被动打开开始接收客户端的请
求,直到客户端返回连接建立成功的信息。在这个过程中,服务器的 socket 进入阻塞状态,也就
是 accept()方法一直到客户端返回连接信息后才返回开始接收下一个客户端的连接请求。
(7)客户端连接成功,向服务器发送连接状态信息。
(8)服务器用 accept() 方法返回,连接成功,接收来自客户端的连接请求。
(9)客户端用 send()方法向 socket 中写入信息。
(10)服务器通过 recv()方法从 socket 中读取信息。
(11)客户端通过 close()方法关闭 socket。
(12)服务器通过 close()方法关闭 socket。
第一次握手:建立连接时,客户端发送 SYN 包(SYN=J)到服务器,并进入 SYNSEND 状态,
等待服务器确认。第二次握手:服务器收到 SYN 包,必须确认客户的 SYN ( ACK=J+1),同时自己
也发送一个 SYN 包( SYN=K),即 SYN+ACK 包,此时服务器进入 SYN RECV 状态。第三次握手:
客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ACK=K+I ),此包发送完毕,客户
端和服务器进入 ESTABLISHED 状态,完成 3 次握手。 网络层的 IP 地址可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机 中的应用程序(进程)。这样利用三元组(ip 地址,协议,端口)就可以标识网络的进程了,网络 中的进程通信就可以利用这个标志与其他进程进行交互 。网络中的进程是通过 socket 来通信的, socket 是“ open-write/read-close ”模式的一种实现,socket 即是一种特殊的文件, 一些 socket 函数就是对其进行的操作(读/写、打开、关闭)。TCP 服务器端依次调用 socket()、bind()、 listen()之后,就会监听指定的 socket 地址了。TCP 客户端依次调用 socket()、connect()之后就 会向 TCP 服务器发送了一个连接请求。TCP 服务器监听到这个请求之后,就会调用 accept()函数 取接收请求,这样连接就建立好了。
2. 主要函数
(1) socket()函数
socket 的函数原型如下所示:socket(int domain,int type,int protocol)。Socket()用于
创建一个 socket 描述符(
socket descriptor),它唯一标识一个 socket。这个 socket 描述符可
以作为参数,通过它来进行一些读写操作。socket 有三个参数:domain、type 和 protocol。其
中,domain 即协议域,又称 family。产用的协议域有: AF _INET、AF_INET6、AF_ LOCAL (或
称 AF_ UNIX, Unix 域 socket)、AF ROUTE 等。协议域决定了 socket 的地址类型、在通信中必
须采用对应的地址,例如 AFINET 决定了要用 ipv4 地址(32 位)与端口号(16 位)的组合,AFUNIX
决定了要用一个绝对路径名作为地址。而type用来指定socket类型。常用的socket类型有: SOCK.
STREAM、SOCK DGRAM、SOCK_ RAW、SOCK_ PACKET、 SOCK_ SEQPACKET 等。
其中,SOCK STREAM 表示提供面向连接的稳定数据传输,即 TCP 协议。SOCK DGRAM 表示
使用不连续、不可靠的数据包连接。最后,
protocol指定了 socket的协议。常用的协议有 IPPROTO_
TCP、IPPTOTO_ UDP、IPPROTO_SCTP、IPPROTO TIPC 等,它们分别对应 TCP 传输协议、
UDP 传输协议、STCP 传输协议、TIPC 传输协议。
当调用 socket 创建一个 socket 时,返回的 socket 描述字它存在于协议域(address family,
AF_ XXX)空间中,但没有一个具体的地址。如果想要给它赋予一个地址,就必须调用 bind()函数,
否则系统就会在调用 connect()、listen() 时自动随机分配一个端口。如果调用成功就返回新创建
的套接字的描述符,如果失败就返回 INVALIDSOCKET( Linux 下失败返回-1 )。套接字描述符是
一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符
和套接字数据结构的对应关系。套接字数据结构都存放在操作系统的内核缓冲里。
(2) bind()函数
bind()的函数原型是:int bind(
int sockfd, const struct sockaddr *addr, socklen_t
addrlen) 。
bind()函数把一个地址域中的特定地址赋给 socket。例如对应 AF INET,
AF_INETT6
就是把一个 ipv4 或 ipv6 地址和端口号组合赋给 socket。函数的有个参数:sockfd、addr 和
addrlen。其中 sockfd 是 socket 描述字,它是通过 socket()函数创建来唯一标识一个 socket 的。
bind()函数就是将给这个描述字绑定一个名字。而 addr 描述了一个 const struct sockaddr*指针,
指向要绑定给 sockfd 的协议地址。这个地址的结构根据地址创建 socket 时的地址协议域的不同
而不同,ipv4 和 ipv6 分别对应不同的代码。例如在本实验中采用 ipv4 协议,对应代码如下:
servaddr.sin_family = AF_INET;
//地址域(指定地址格式) ,设为 AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //AF_INET:所有 IP 都可以连上
servaddr.sin_port = htons(6666);
//端口号
最后,addrlen 对应的是地址的长度。通常服务器在启动的时候都会绑定一个众所周知的地址
(如 ip 地址+端口号)。用于提供服务。客户就可以通过它来连接服务器,而客户端就不用指定,有 系统自动分配一个端口号和自身的 IP 地址组合。这就是为什么通常服务器端在调用 listen 之前会
调用 bind(),而客户端就不用调用,而是在 connect()时由系统随机生成一个。返回值:如果函数执
行成功,返回值为 0,反之为 SOCKET ERROR。
(3) listen()函数和 connect() 函数
listen()和 connect()函数的原型是:int listen(int sockfd , int backlog)和 int connect(int
sockfd , const struct sockaddr *addr, socklen_t addr len)。服务器在调用 socket()和 bind()
函数之后就会调用 listen()函数来监听这个 socket,如果客户端这时调用 connect()发出连接请求,
服务器端就会接收到这个请求从而建立连接。
listen()函数的第一个参数即为要监听的 socket 描述字,第二个参数为相应 socket 可以排队
的最大连接个数。socket()函数创建的 socket 默认是一个主动类型的,listen 函数将 socket 变为
被动类型的,等待客户的连接请求。connect()函数的第一个参数即为客户端的 socket 描述字,第
二参数为服务器的 socket 地址,第三个参数为 socket 地址的长度。客户端通过调用 connect()
函数来建立与 TCP 服务器的连接。
(4) accept()函数
建立好连接后,就可以开始网络 I/0 操作了,accept 的函数原型是:int accept(int sockfd,
struct sockaddr *addr, socklen_t *addrl en)。accept 函数的第一个参数为服务器的 socket 描
述字;第二个参数为指向 struct sockaddr*的指针,用于返回客户端的协议地址;第三个参数为
协议地址的长度。如果 accpet 成功,那么其返回值是由内核自动生成的一个全新的描述字,代表
与返回客户的 TCP 连接。一个服务器通常仅仅只创建一个监听 socket 描述字,它在该服务器的生
命周期内一直存在。内核为每个由服务器进程接受的客户创建了一个已连接 socket 描述字,当服
务器完成了对某个客户的服务,相应的已连接 socket 描述字就被关闭。
(5) read() 和 write() 函数
至此服务器与客户已经建立好连接了,可以调用网络 I/0 进行读写操作了,即实现了网络中
不同进程之间的通信。网络 I/0 操作有下面几组:
read()/write();
recv()/send();
readv()/writev();
recvmsg()/sendmsg();recvfom()/sendto()。
最常用的则是 read()和 write()。read() 的函数原型是:ssize_ t read(int fd, void *buf, size_ t
count)read()函数是负责从 fd 中读取内容。当读取成功时,read()返回实际所读的字节数,如果返
回的值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误。如果错误为 EINTR 说明读是由
中断引起的,如果是 ECONNREST 表示网络连接出了问题。三个参数分别是代表 socket 描述字
(fd);缓冲区(buf);缓冲区长度(count)。write()的函数原型是:ssize_ t write(int fd, const
void *buf, size_ t count)。write()函数将缓冲区中的 nbytes 字节内容写人文件描述符 fd 成功时
返回写的字节数。失败时返回-1,并设置 errmo 变量。在网络程序中,当我们向套接字文件描述
符写时有两种可能:①write 的返回值大于 0,表示写了部分或者是全部的数据;②返回的值小于 0,
此时出现了错误。实际中要根据错误类型来处理。如果错误为 EINTR 表示在写的时候出现了中断
错误。如果为 EPIPE 表示网络连接出现了问题(对方已经关闭了连接)。
(6) close()函数
完成了读写操作就要关闭相应的socket 描述字。
close 的函数原型是:int close (int fd)。close 操作一般使相应 socket 描述字的引用计数值为-1,只有当引用计数为 0 的时候,才会触发 TCP
客户端向服务器发送终止连接请求。
3. 代码实现
(1) 服务器端 首先建立一个 socket 描述符,设置套接字的类型为:SOCK_STREAM,即表示提供面向连接 的稳定数据传输,即 TCP 协议。并把一个地址域中的特定地址赋给 socket。对应 AF INET,即 把一个 ipv4 地址和端口号组合赋给 socket。对应代码:listenfd = socket(AF_INET,
SOCK_STREAM, 0。接着,设置可连接的 ip 为任意 ip 值:sin_addr.s_addr =
htonl(INADDR_ANY),设置端口号:sin_port = htons(6666)。然后,使用 bind()函数将 servaddr
地址绑定到该 socket 描述符:bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))
== -1,再用 listen()函数让服务器处于监听模式等待客户端的连接,并设置最大连接数。接着使用
while 循环接受客户端的请求,在循环里用指向 client_addr 的指针来获取对方的地址 connfd =
accept(listenfd, (struct sockaddr*)&client_addr, &size)) == -1,然后用 read()函数接收客户
端发来的数据并重写缓冲区。最后结束连接,关闭套接字。
(2) 客户端
首先创建套接字并设置相关的协议,地址域、端口号及地址格式,并设置在终端获取客户端所
要连接的 ip 地址。然后采用 inet_pton()函数将字符串转换成网络地址,并复制到服务器的
sin_addr 中:inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0。接着用 connnect()
函数将两者连接起来,连接成功后,读取文件内容(/home/kkk/桌面/tt.jpg),发送给服务器端。
服务器端新建 tt.jpg 文件,将接收到的文件内容保存到 tt.jpg 中,并设置 tt.png 位于当前目录下。
最后完成连接,关闭套接字。

client.cpp为客户端实现代码
sever.cpp为服务器端实现代码
执行make命令后,生成server和client两个可执行文件。分别打开两个终端窗口,分别执行./server命令和./client 127.0.0.1命令,表示连上本机的6666端口,./server命令的首先执行。执行./client 127.0.0.1命令后,client客户端执行完毕直接退出,这时server的终端窗口输出“recv msg from client:”。打开server.cpp文件所在的目录,可看到tt.jpg文件已经生成。

client.cpp

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#define MAXLINE 4096

int main(int argc, char** argv){
    int   sockfd, len;
    char  buffer[MAXLINE];
    struct sockaddr_in  servaddr;    
    FILE *fq;

	//创建套接字,打印客户端所要连接的服务器ip地址:127.0.0.1
    if( argc != 2){
        printf("usage: ./client <ipaddress>\n");
        return 0;
    }

    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
        printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
        return 0;
    }


	//设置远程地址信息
    memset(&servaddr, 0, sizeof(servaddr));	//先把servaddr地址清空,再复制
    servaddr.sin_family = AF_INET;		//地址族(指定地址格式) ,设为AF_INET
    servaddr.sin_port = htons(6666);		//连接服务器端口号



	// arg[1]是写ip地址的地方,inet_pton 是 IP 地址转换函数,可以在将 IP 地址在“点分十进制”和“二进制整数”之间转换
	//inet_pton函数的原型是:int inet_pton(int af,const char *src,void *dst)
	//这个函数将字符串转换成网络地址,第一个参数af=AF_INET,,src指向ASCII的地址的首地址(x.x.x.x的格式)
	//函数将该地址转换成in_addr的结构体并复制到*dst,即sin_addr中
    if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
        printf("inet_pton error for %s\n",argv[1]);
        return 0;
    }

    if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
        printf("connect error: %s(errno: %d)\n",strerror(errno),errno);		//连接失败响应函数
        return 0;
    }


	//连接成功后,读取文件内容(/home/kkk/桌面/tt.jpg),发送给服务器端
    if( ( fq = fopen("/home/kkk/桌面/tt.jpg","rb") ) == NULL ){
        printf("File open.\n");
        close(sockfd);
        exit(1);
    }

    bzero(buffer,sizeof(buffer));


	//服务器端新建tt.png文件,将接收到的文件内容保存到tt.png中,tt.jpg在当前目录下;
    while(!feof(fq)){
        len = fread(buffer, 1, sizeof(buffer), fq);
        if(len != write(sockfd, buffer, len)){
            printf("write.\n");
            break;
        }
    }
    close(sockfd);  // 关闭套节字 
    fclose(fq);

    return 0;
}

sever.cpp

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#define MAXLINE 4096

int main(int argc, char** argv){
    int  listenfd, connfd;
    struct sockaddr_in  servaddr;
    char  buff[4096];
    FILE *fp;
    int  n;
    
    //指定套接字的类型: SOCK_STREAM(即TCP协议,一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。)
    //建立一个socket描述符,socket(ipv4,提供面向连接的稳定传输数据,指定协议为0),AFINET决定了要用ipv4地址(32位)与端口号(16位)的组合
    if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
        printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
        return 0;						//如果连接错误,打印错误提示
    }
    printf("----init socket----\n");  //初始化套接字

    memset(&servaddr, 0, sizeof(servaddr));		//先把servaddr地址清空,再复制
    servaddr.sin_family = AF_INET;			//套接字使用的地址族(指定地址格式):ipv4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);	//AF_INET表示什么IP都可以连上
    servaddr.sin_port = htons(6666);			//端口号6666,1024 ~ 49151:普通用户注册的端口号


   	 //设置端口可重用
    int contain;
    setsockopt(listenfd,SOL_SOCKET, SO_REUSEADDR, &contain, sizeof(int));

	将servaddr地址绑定到该socket描述符
    if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
        printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
        return 0;
    }
    printf("----bind sucess----\n");



	//进入监听模式,监听这个socket描述符,10这里指的是监听队列中允许保持的尚未处理的最大连接数是10
    if( listen(listenfd, 10) == -1){
        printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
        return 0;
    }



    if((fp = fopen("tt.jpg","ab") ) == NULL )
    {
        printf("File.\n");
        close(listenfd);
        exit(1);
    }

    printf("======waiting for client's request======\n");


	// 循环接受客户的连接请求 
    while(1){
        struct sockaddr_in client_addr;
        socklen_t size=sizeof(client_addr);

		//一个指向client_addr的指针用于获取对方的地址
        if( (connfd = accept(listenfd, (struct sockaddr*)&client_addr, &size)) == -1){
            printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
            continue;
        }

		//从客户端接收数据
        while(1){

            n = read(connfd, buff, MAXLINE);  //recv 函数接收到的字符串是不带 ”\0”结束符的
            if(n == 0)
                break;
            fwrite(buff, 1, n, fp);
        }
        buff[n] = '\0';
        printf("recv msg from client: %s\n", buff); 要用 printf输出时必须得先加上结束符”\0"
        close(connfd);
        fclose(fp);
    }


		/*在 while 循环里持续接收包,注意, accept 和 read 是都是在 while 循环里的,
		也就是收到包之后,listenfd就没用了,并关闭它,下一个包重新接收包 。 
		*/
    close(listenfd);   //关闭监听套节字
    return 0;
}

运行结果:

参考文章:

Linux C/C++ TCP Socket传输文件或图片实例 - zkfopen - 博客园

Socket原理及实践(Java/C/C++) - xiuzhublog - 博客园

 Linux C/C++ TCP Socket通信实例 - zkfopen - 博客园

  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值