[linux-socket]UNIX网络编程之TCP连接

1、常用函数介绍

int socket(int domain,int type,int protocol);
/*
domain:AF_INET设为IPV4
type:SOCK_STREAM对应TCP,SOCK_DGRAM对应UDP
protocol:设0
返回值:返回一个套接字,失败返回-1
*/
int bind(int sockfd,struct sockaddr *my_addr,int addrlen);
/*
sockfd:由socket()调用返回的需要绑定的套接字
my_addr:sockaddr类型的地址
addrlen:sizeof(sockaddr)。
返回值:成功返回0;失败返回-1
*/
struct sockaddr_in {
short sin_family; /* 地址类型,TCPIP协议只能填AF_INET */
unsigned short sin_port; /*使用端口号 */
struct in_addr sin_addr; /* 网络地址,如需绑定所有地址,填INADDR_ANY */
unsigned char sin_zero[8]; /* 填0即可 */
};//一般将其强制转换成sockaddr来使用
uint16_t htons(uint16_t hostshort);
/*
把系统的16位整数调整为“大端模式”
*/
int inet_aton(const char *string, struct in_addr *addr);
/*
把字符串的IP地址转化为in_addr结构体
*/
int connect(int sockfd,struct sockaddr* serv_addr,int addrlen);
/*
sockfd:连接到的套接字
serv_addr:连接到的服务器地址
addrlen:sizeof(serv_addr)
返回值:失败返回-1
*/
int listen(int sockfd,int backlog);//设置服务器监听模式
/*
sockfd:需要设置监听的服务器套接字
backlog:进入队列中允许的连接的个数。
返回值:出错返回-1
*/
int accept(int sockfd,void *addr,int* addrlen);//接受已经connect并在款冲队列中等待的套接字,队列为空时默认进入阻塞状态,直到有客户端进行connect()
/*
sockfd:正在监听端口的套接字
addr:用于存储客户的地址结构体
addrlen:sizeof(struct sockaddr_in)
返回值:失败-1
*/
int send(int sockfd,const void* msg,int len,int flags);//TCP发送数据
/*
sockfd:发送目标的套接字
msg:需要发送数据的头指针
len:数据的字节长度
flags:设为0
返回值:返回实际发送的字节数。注意,返回值可能比需要发送的字节数要少,此时需要再次发送剩下的字节。如失败返回-1
*/
int recv(int sockfd,void* buf,int len,unsigned int flags);//接受TCP数据
/*
sockfd:是要读取的套接口字
buf:保存数据的内存入口。
len:缓冲区的最大长度。注意,缓冲区不需用完
flags:设为0
返回值:返回实际读取到缓冲区的字节数,如果出错则返回-1。
*/

2、服务器端流程

设置服务器地址

 //设置一个socket地址结构server_addr,代表服务器internet地址, 端口
    struct sockaddr_in server_addr;
    bzero(&server_addr,sizeof(server_addr)); //把一段内存区的内容全部设置为0
    server_addr.sin_family = AF_INET;//
    server_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY是全0特殊地址,用于含有多IP地址的服务器,表示同时绑定自己的所有地址
    server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);//小端数转大端数函数

创建服务器套接字

//创建用于internet的流协议(TCP)socket,用server_socket代表服务器socket
    int server_socket = socket(AF_INET,SOCK_STREAM,0);
    if( server_socket < 0){
        printf("Create Socket Failed!");
        exit(1);
    }

绑定地址与套接字

//把socket和socket地址结构联系起来
    if( bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr))){
        printf("Server Bind Port : %d Failed!", HELLO_WORLD_SERVER_PORT); 
        exit(1);
    }

设置服务器为监听状态

//server_socket用于监听
    if ( listen(server_socket, LENGTH_OF_LISTEN_QUEUE) ){
        printf("Server Listen Failed!"); 
        exit(1);
    }

接受客户端的连接

int new_server_socket = accept(server_socket,(struct sockaddr*)&client_addr,&length);
        if ( new_server_socket < 0) {
            printf("Server Accept Failed!
");
            break;
        }

从客户端接收数据

//接收客户端发送来的信息到buffer中
        length = recv(new_server_socket,buffer,BUFFER_SIZE,0);//如果对方在一次连接里发送了两次呢?
        if (length < 0){
            printf("Server Recieve Data Failed!
");
            exit(1);
        }
        printf("
%s",buffer);

发送数据到客户端

char buffer[BUFFER_SIZE];
        bzero(buffer, BUFFER_SIZE);
        strcpy(buffer,"Hello,World! 从服务器来!");
        strcat(buffer,"
"); //C语言字符串连接
        //发送buffer中的字符串到new_server_socket,实际是给客户端
        send(new_server_socket,buffer,BUFFER_SIZE,0);

关闭连接,空出端口

    //关闭与客户端的连接
    close(new_server_socket);
    //关闭监听用的socket
    close(server_socket);

3、客户端流程

设置客户端地址

//设置一个socket地址结构client_addr,代表客户机internet地址, 端口
    struct sockaddr_in client_addr;
    bzero(&client_addr,sizeof(client_addr)); //把一段内存区的内容全部设置为0
    client_addr.sin_family = AF_INET;    //internet协议族
    client_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY表示自动获取本机地址
    client_addr.sin_port = htons(0);    //0表示让系统自动分配一个空闲端口

建立客户端的套接字

//创建用于internet的流协议(TCP)socket,用client_socket代表客户机socket
    int client_socket = socket(AF_INET,SOCK_STREAM,0);
    if( client_socket < 0) {
        printf("Create Socket Failed!
");
        exit(1);
    }

绑定客户端地址与套接字

//把客户机的socket和客户机的socket地址结构联系起来
    if( bind(client_socket,(struct sockaddr*)&client_addr,sizeof(client_addr))){
        printf("Client Bind Port Failed!
"); 
        exit(1);
    }

设置服务器地址结构体

//设置一个socket地址结构server_addr,代表服务器的internet地址, 端口
    struct sockaddr_in server_addr;
    bzero(&server_addr,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    if(inet_aton(argv[1],&server_addr.sin_addr) == 0){ //服务器的IP地址来自程序的参数,aton字符串IP地址转化为网络地址格式
        printf("Server IP Address Error!
");
        exit(1);
    }
    server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);

建立于服务器的连接

socklen_t server_addr_length = sizeof(server_addr);
    //向服务器发起连接,连接成功后client_socket代表了客户机和服务器的一个socket连接
    if(connect(client_socket,(struct sockaddr*)&server_addr, server_addr_length) < 0)
    {
        printf("Can Not Connect To %s!
",argv[1]);
        exit(1);
    }

接收、发送数据

关闭连接

 完整代码:https://github.com/iyjhabc/study_examples/blob/master/server.c

编译c程序 gcc client.c -o client

运行客户端 ./client 127.0.0.1

127.0.0.1为保留IP地址,固定指向本地主机

----------------------------------------------------------------------------------------------------

虽然知道各个函数怎么用,但这几个函数都是要组合起来使用,组合得不对使用就会出错,下面介绍一下如何才是正确组合TCP函数。

  两个计算机通过网络连接,说白了通过两样东西来定位,IP和端口。套接字其实就是一个已经连接到某IP和某端口的一条抽象通道。知道这两个概念,下面就容易理解了。

1、服务器

  作为服务器,一般来说不会首先连接别人,而是等别人主动连接它,他被动等待别人的连接。并且作为公共使用的服务器,必须有固定的端口,否则别人怎么知道怎么找到你?

  (1)bind().服务器的第一步是把新建的服务器socket套接字bind一个端口。此时此套接字已经跟服务器的IP和端口紧紧联系在一起了(但还没连接)。

  (2)listen()与accept().此二函数是一起使用的,首先把服务器的套接字设为监听状态,然后在循环里面调用accept。它被调用后会阻塞自己所在的线程,直到有客户端connect为止。

  (3)recv().当有客户端连接服务器,accept的阻塞被释放,并返回一个已经连向该客户端的套接字。此时服务器就可以通过此套接字,使用recv函数接受来自客户端的数据,并使用send给客户端发送数据。

  (4)close().accept返回的套接字使用完成后,必须使用close把它关闭。

  总结来说,服务器只需自己绑定一个端口,等待客户端的连接。它完全不用管客户端的ip和端口,因为accept返回的套接字已经包含了连接向客户端IP和端口的连接通路。

2、客户端

  作为客户端,一般是主动连接服务器,等待服务器的回应。客户端必须知道想要连接的服务器的IP和端口。

  (1)connect().首先客户端新建一个套接字,并根据服务器的IP和端口把套接字connect到服务器,形成连接通路。

  (2)send().连接成功后,就可以利用该套接字向服务器发送数据。

  (3)recvfrom().因为刚才的套接字已经与服务器形成连接,因此也可以用来接收服务器返回的数据。

  这里再说明一下,connect形成与服务器连接这个套接字,其实就是服务器端accept返回那个套接字,正因为如此,客户端才能用这个套接字recv服务器返回的消息。至于为什么用recvfrom而不是recv,接下来说。

  服务器与客户端握手对话(客发-服收-服发-客收)代码可参考以下:

客户端:https://github.com/iyjhabc/study_examples/blob/master/tcp_data_transport_client.c

服务器:https://github.com/iyjhabc/study_examples/blob/master/tcp_data_transport.c

3、recv与recvfrom

  众所周知recv主要用在TCP中,recvfrom主要用在UDP中,但如客户端需要等待服务器返回的消息,再作下一步运算的话,就应使用recvfrom,如上面的例子,客户端等待服务器放置数据完成,再重新发送下一轮数据。原因是,recv默认是非阻塞的,当客户端发送完后调用recv,服务器还没发送数据过来recv就已经执行完毕,因此接受不到服务器数据。recvfrom默认是阻塞的,它会等待服务器返回的数据再往下运行。

  并且recv只能用于TCP,而recvfrom同时用于TCP和UDP。recvfrom参数的地址指针将会记录接到的数据的来源地址。再有一点,recv和recvfrom参数里面的套接字,必须是已经建立连接的,没连接,怎么知道接受谁呢?要监听任意的客户端,应先使用listen和accept。

4、send

  一般来说,使用一次send,便connect一次。如连续使用send,后面send的数据会丢失。为何?其实联系服务器的原理便知道。客户端connect后,服务器accept并返回一个连接双方的套接字。此时双方用此套接字发送接收数据。但一般来说服务器recv一次后便会close该套接字。如果此时客户端继续试用send,自然就发送不成功了。

  总结就是,必须在套接字连同的条件下才能使用send(也是recv也是),很多情况是你自己没有断开连接,但对方其实已经close了该套接字了,便造成了发送失败。所以,连接一次发送一次数据send一次,是比较安全的做法。

原文地址:https://www.cnblogs.com/iyjhabc/p/3155141.html