Linux TCP套接字編程

Linux TCP套接字編程


TCP套接字編程的基本步驟

服務端編程的步驟如下:

  1. 創建服務端套接字。
  2. 綁定套接字到一個IP地址和一個端口上(使用bind函數)。
  3. 將套接字設置爲監聽模式等待連接請求(使用函數listen),這個套接字就是監聽套接字了。
  4. 請求到來後,接受連接請求。返回一個新的對應此連接的套接字。
  5. 用返回的新的套接字和客戶端進行通信,即發送或接收數據(使用send 或者 recv 函數),通信結束就關閉這個新創建的套接字(使用函數 closesocket)。
  6. 監聽套接字繼續處於監聽狀態,等待其他客戶端的連接請求。
  7. 如果要退出服務器程序,則先關閉監聽套接字(使用函數closesocket)。

客戶端編程的步驟如下:

  1. 創建客戶端套接字(使用函數 socket)。
  2. 向服務器發出連接請求(使用函數 connect)。
  3. 和服務器端進行通信,即發送或接收數據(使用函數 send 或 recv)。
  4. 如果要關閉客戶端程序,則先關閉套接字(使用函數closesocket)。

什麼是阻塞與非阻塞套接字?

  當使用函數 socket 創建套接字時,默認都是阻塞模式。阻塞模式是指套接字在執行操作時,調用函數在沒有完成操作之前不會立即返回。也就是說,調用 API 函數時,不能立即完成,線程需要處於等待狀態,直到操作完成。但並不是調用所有的API函數都會阻塞,例如 bind(),listen(),即使用阻塞套接字當參數傳入,函數也會立即返回,不需要等待。

以下 Linsock API 使用阻塞套接字當參數調用函數時,會發生阻塞。

  1. 接收連接函數

    函數accept從請求連接隊列中接收一個客戶端連接。若請求隊列爲空,則函數就會阻塞,線程進入睡眠狀態。

  2. 發送函數

    函數send、sendto 都是發送數據函數。如果套接字緩衝區沒有可用空間,函數就會阻塞,線程就會睡眠,直到緩衝區有空間。

  3. 接收函數

    函數recv 用來接收數據。如果此時套接字緩衝區沒有數據可讀,則函數阻塞,調用線程在數據到來前處於睡眠狀態。

  4. 連接函數

    函數 connect 用於向對方發送連接請求。發出連接時,直到收到服務器的應答或超時纔會返回。

下面就用一個簡單的服務器客戶機聊天程序(非阻塞套接字版)來進行演示TCP通信。

先創建服務端程序:

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

int main()
{
    int sfp,nfp;
    struct sockaddr_in sAddr,cAddr;
    socklen_t sinSize;
    unsigned short portNum = 10051;
    printf("info: Hello, I am a server, Welcome to connect me!\n");
    sfp = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sfp)
    {
        printf("error: socket fail!\n");
        return -1;
    }
    printf("info: socket ok!\n");

    int on = 1;
    setsockopt(sfp, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); //  允許地址可立即重用

    bzero(&sAddr, sizeof(struct sockaddr_in));
    sAddr.sin_family = AF_INET;
    sAddr.sin_addr.s_addr = htonl(INADDR_ANY);	//	INADDR_ANY = 0.0.0.0 
    sAddr.sin_port = htons(portNum);	//	htons 主機字節序轉換成網絡字節序

    if (-1 == bind(sfp, (struct sockaddr *)(&sAddr), sizeof(struct sockaddr)))
    {
        printf("error: bind fail:%d\n", errno);
        return -1;
    }
    printf("info: bind ok!\n");

    if (-1 == listen(sfp, 5))	//	5是最大連接數
    {
        printf("error: listen fail!\n");
        return -1;
    }
    printf("info: listen ok!\n");

    while (1)
    {
        sinSize = sizeof(struct sockaddr_in);
        nfp = accept(sfp, (struct sockaddr *)(&cAddr), &sinSize);
        if (-1 == nfp)
        {
            printf("error: accept fail!\n");
            return -1;
        }
        printf("info: accept ok!\n");
        printf("info: Server start to connect from ip=%s, port=%d\n",
                inet_ntoa(cAddr.sin_addr),
                ntohs(cAddr.sin_port));	//	inet_ntoa 將二進制地址轉換成點分十進制地址

        if (-1 == write(nfp, "Hello, client, you are welcome!\n", 32))
        {
            printf("error: write fail!\n");
            return -1;
        }
        printf("info: write ok!\n");
        close(nfp);

        puts("continue to listen(y/n)?");
        char ch[2];
        scanf("%s", ch, 2);
        if (ch[0] != 'y')
            break;
    }

    printf("info: bye!\n");
    close(sfp);
    return 0;
}

服務端先新建一個監聽套接字,然後等待客戶端的連接請求,阻塞在accept函數處,一旦有客戶端連接請求來了,就返回一個新的套接字。這個套接字就和客戶端進行通信,通信完畢後關掉這個套接字。而監聽套接字根據用戶的輸入繼續監聽或退出。

客戶端程序:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include "unistd.h"
#include <string.h>

const int BUFFER_SIZE = 1023;

//	將套接字設置爲非阻塞
int SetNonBlocking(int fd)
{
    int oldOption = fcntl(fd, F_GETFL);
    int newOption = oldOption | O_NONBLOCK;
    fcntl(fd, F_SETFL, newOption);
    return oldOption;
}

//	非阻塞連接,time是超時時間。
int UnblockConnect(const char* ip, int port, int time)
{
	//	初始化套接字地址
    int ret = 0;
    struct sockaddr_in Address;
    bzero(&Address, sizeof(struct sockaddr_in));
    Address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &Address.sin_addr);
    Address.sin_port = htons(port);

	//	創建非阻塞套接字,用於客戶端連接
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    int fdopt = SetNonBlocking(sockfd);
    ret = connect(sockfd, (struct sockaddr *)(&Address), sizeof(struct sockaddr));
    printf("info: ret code = %d\n", ret);
    if (0 == ret)	//	創建非阻塞套接字,用於客戶端連接
    {
        printf("info: connect with server immediately\n");
        fcntl(sockfd, F_SETFL, fdopt);
        return sockfd;
    }
    else if (EINPROGRESS != errno)	//	連接失敗
    {
        printf("error: unblock connect failed!\n");
        return -1;
    }
    else if (EINPROGRESS == errno)	//	連接中
    {
        printf("info: unblock mode socket is connecting ...\n");
    }

	//	調用select函數判斷連接是否成功
	//	如果套接字是 writable,那麼套接字連接成功
    fd_set readfds;
    fd_set writefds;
    struct timeval timeout;
    FD_ZERO(&readfds);
    FD_SET(sockfd, &writefds);

	//	設置超時時間
    timeout.tv_sec = time;
    timeout.tv_usec = 0;

    ret = select(sockfd + 1, NULL, &writefds, NULL, &timeout);
    if (ret < 0)
    {
        printf("error: connection time out\n");
        close(sockfd);
        return -1;
    }

    if (!FD_ISSET(sockfd, &writefds))
    {
        printf("info: no events on sockfd found\n");
        close(sockfd);
        return -1;
    }

	//	如果套接字描述符可讀或可寫,則我們用getsockopt來得到套接字上待處理的錯誤
	//	如果連接建立成功,這個錯誤值將是0。
    int error = 0;
    socklen_t length = sizeof(error);
    if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0)
    {
        printf("error: get sock option failed\n");
        close(sockfd);
        return -1;
    }
    if (0 != error)
    {
        printf("error: connection failed after select with the error:%d\n", error);
        close(sockfd);
        return -1;
    }

    printf("info: Connection ready after  select with the socket:%d\n",sockfd);
    fcntl(sockfd, F_SETFL, fdopt);	//	將套接字恢復成原來屬性
    printf("info: Connect ok!\n");

    int recvBytes;
    int sinSize;
    char buf[1024] = {0};
    recvBytes = read(sockfd, buf, 1024);	//	接收數據
    if (-1 == recvBytes)
    {
        printf("error: read data fail!\n");
        return -1;
    }
    printf("info: read ok!\n");

    buf[recvBytes] = '\0';
    printf("%s\n", buf);

    return sockfd;
}

int main()
{
    int sockfd = UnblockConnect("127.0.0.1", 10051, 1);
    if (0 > sockfd)
    {
        printf("error: create socket fail!\n");
        return -1;
    }
    close(sockfd);
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章