Linux系统C++编程实现TCP通信

原文链接: blog.csdn.net/Vickers_x

TCP通信过程

服务器在明处,客户端在暗处。 对于客户端来说,客户端有服务器的IP地址和端口号,而对于服务器来讲,服务器不知道客户端在哪里,只有当客户端主动发送请求之后才能知道客户端的IP和端口。

TCP请求由客户端发起。 当客户端需要服务器的服务时候,客户端要首先建立连接。在建立连接之前要创建套接字socket。

客户端不建议绑定地址 ,因为对于客户端,我并不关心我的数据使用哪个地址哪个端口发送。而且我也不能实时了解操作系统的哪个端口处于闲置状态。所以我不绑定地址和端口,操作系统可以给我分配端口,并添加IP地址。

服务器必须绑定地址 这些socket都是由服务器自己创建并绑定地址的,而这个IP地址和端口都是唯一确定的。(服务器地址如果修改,就需要对原地址进行重定向,否则,客户端会找不到服务器。)所以在socket创建好之后,就必须绑定地址。

客户端连接之前,服务器一直有socket在等待 ,服务端的任务就是提供服务,按理说,服务器应该实时具有处于LISTEN状态的socket,以保证客户端的连接请求都能被处理。

1、创建套接字

建立与网卡的关联,协议版本的选择,传输层协议的选择

int socket(int domain, int type, int protocol);

domain: 地址域
AF_INET(协议类型)一般使用ipv4协议

type: 套接字类型
SOCK_STREAM 流式套接字
SOCK_DGRAM 数据报套接字

protocol(协议类型):0-默认;流式套接字默认 TCP 协议 IPPROTO_TCP,数据报套接字默认 UDP 协议IPPROTO_UDP

返回值:套接字描述符,失败:-1

On  success, a file descriptor for the new socket is returned.  On error, -1 is
       returned, and errno is set appropriately.

2、为套接字绑定地址信息(ip、port)

bind函数既可以绑定IPV4版本协议,也可以绑定IPV6版本,两个版本的IP头结点的大小不同,定义sockaddr为了寻求接口的统一,都要使用 struct sockaddr

struct sockaddr_in		//IPV4
struct sockaddr_in6		//IPV6 

为socket绑定地址信息,确定socket能够操作缓冲区中的哪些数据

int bind(int sockfd, struct sockaddr *addr,socklen_t addrlen);
  • sockfd: 套接字描述符
  • addr: 要绑定的地址信息
  • addrlen:地址信息的长度

3、监听 对于服务器而言

int listen(int sockfd, int backlog);

sockfd:sockfd描述符,backlog:最大同时并发连接数(已完成连接队列中允许存放的socket个数)

半连接的socket:当客户端发送来SYN连接请求时,服务器知道了客户端想要建立连接。可是客户端并不知道自己的连接请求是否已经到达服务器。这个状态下的连接处于半连接,服务器会将自己的这个处于半连接的请求放入一个半连接请求的队列来维护。同时发送给客户端ACK+SYN,告诉客户端“你发送的连接请求我已经收到了,我也要建立连接了。”。

已经完成连接的socket:当客户端收到服务器发送的ACK和SYN之后,客户端会给服务一个应答报文ACK,服务器接受到这个报文之后,服务器关于这个连接的socket会放入已完成连接队列进行维护。这个时候,服务器才会为这个连接开辟一个数数据结构,开辟更多资源,以满足这个连接的服务。

无论是已经完成连接接的socket还是处于半连接的socket,操作系统都要有一个队列去维护。

4、连接请求 对于客户端而言

 int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  • sockfd:客户端的socket文件描述符
  • addr:要连接的服务端地址
  • addrlen:要连接的服务端的socket结构体的长度,单位是字节
  • 返回值:成功:0,失败:-1

5、接受连接请求 对于服务器而言

accept函数是一个阻塞型的函数,连接成功队列中如果没有新的连接到来,那么就会一直阻塞直到有新的客户端连接到来

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:这里的sockfd是服务器新创建的socket的描述符
  • addr:新建立连接的客户端地址信息
  • addrlen:客户端的socket结构体的长度,单位是字节
  • 返回值
    成功:返回新的socket描述符 失败返回:-1
On success, these system calls return a nonnegative integer that is a  descriptor
 for the accepted socket.  On error, -1 is returned, and errno is set appropriately.

当服务端成功接受连接请求之后,会为新连接重新创建一个socket用于专门和这个连接的通信,而这个重新创建的socket是由accept这个函数完成的。 而原来socket函数返回的socket只是用于接受连接,并不用来进行通信。

6、接收/发送数据

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd:连接成功的socket描述符
  • 返回值
    错误:-1,连接关闭:0,成功: 实际接受长度 >0
  • flags:默认0-阻塞式接收
  • buf: 用于指定接收数据的存放位置
  • len: 用于指定希望接收的数据长度

7、关闭套接字

int close(int fd);

我们说socket函数的返回值是socket描述符,实际也是文件描述符。因为系统的文件描述符有限,我们不再使用这个文件时候,就应该关闭这个描述符,否则可能造成文件描述符泄漏问题。

基于TCP协议的 应用层 聊天程序

TCP服务端

创建套接字——>绑定地址——>监听——>accept——>recv——>send——>close。

//tcp服务端的代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
int main()
    //1、创建socket套接字
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0){
        perror("socket error");
        return -1;
    struct sockaddr_in ser;
    ser.sin_family=AF_INET;
    ser.sin_port=htons(atoi("9000"));
    ser.sin_addr.s_addr=inet_addr("192.168.117.130");
    //2、绑定地址
    socklen_t len=sizeof(struct sockaddr_in);
    if(bind(sockfd,(struct sockaddr*)&ser,len)<0)
        perror("bind error");
        close(sockfd);
        return -1;
    //3、监听
    if(listen(sockfd,5)<0)
        perror("listen error");
        close(sockfd);
        return -1;
    while(1)
        int n_sockfd;
        struct sockaddr_in cli;
        //4、accept
        n_sockfd=accept(sockfd,(struct sockaddr*)&cli,&len);
        if(n_sockfd<0){
            perror("accept error");
            continue;
        //5、recv
        while(1){
            //6、send
            char buff[1024]={0};
            int ret=recv(n_sockfd,buff,1023,0);
            if(ret<0){
                perror("recv error");
                continue;
            printf("client %s[%d]say:%s",inet_ntoa(cli.sin_addr),ntohs(cli.sin_port),buff);
            memset(buff,0x00,1024);
            scanf("%s",buff);
            send(n_sockfd,buff,sizeof(buff),0);
            //7、close(sockfd)
        close(n_sockfd);

TCP客户端

不推荐手动绑定地址

创建套接字——>connect——>send——>recv——>close。

//tcp客户端端的代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
int main()
    //1、创建socket套接字
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0){
        perror("socket error");
        return -1;
    struct sockaddr_in ser;
    ser.sin_family=AF_INET;
    ser.sin_port=htons(atoi("9000"));
    ser.sin_addr.s_addr=inet_addr("192.168.117.130");
    //2、连接
    socklen_t len=sizeof(struct sockaddr_in);
    if(connect(sockfd,(struct sockaddr*)&ser,len)<0)
        perror("connect error");
        return -1;
    //3、发送数据
    while(1)
            char buff[1024]={0};
            scanf("%s",buff);
            send(sockfd,buff,sizeof(buff),0);
            memset(buff,0x00,1024);
            int ret=recv(sockfd,buff,1023,0);
            if(ret<0){
                perror("recv error");
                continue;