工学1号馆

home

基本TCP套接字编程

Wu Yudong    October 25, 2016     Linux/Unix   583   

1、socket函数

套接字是通信端点的抽象,实现端对端之间的通信。与应用程序要使用文件描述符访问文件一样,访问套接字需要套接字描述符。任何套接字编程都必须调用socket 函数获得套接字描述符,这样才能对套接字进行操作。以下是该函数的描述:

#include <sys/socket.h> 
int socket(int family, int type, int protocol);

函数功能:创建套接字描述符;

返回值:若成功则返回套接字非负描述符,若出错返回-1;

说明:

socket类似与open对普通文件操作一样,都是返回描述符,后续的操作都是基于该描述符;

family 表示套接字的通信域,不同的取值决定了socket的地址类型,其一般取值如下:

(1)AF_INET         IPv4因特网域

(2)AF_INET6        IPv6因特网域

(3)AF_UNIX         Unix域

(4)AF_ROUTE        路由套接字

(5)AF_KEY          密钥套接字

(6)AF_UNSPEC       未指定

type确定socket的类型,常用类型如下:

(1)SOCK_STREAM     有序、可靠、双向的面向连接字节流套接字

(2)SOCK_DGRAM      长度固定的、无连接的不可靠数据报套接字

(3)SOCK_RAW        原始套接字

(4)SOCK_SEQPACKET  长度固定、有序、可靠的面向连接的有序分组套接字

protocol指定协议,常用取值如下:

(1)0               选择type类型对应的默认协议

(2)IPPROTO_TCP     TCP传输协议

(3)IPPROTO_UDP     UDP传输协议

(4)IPPROTO_SCTP    SCTP传输协议

(5)IPPROTO_TIPC    TIPC传输协议

本文地址:http://wuyudong.com/2016/10/25/2906.html,转载请注明源地址。

2、connect 函数

在处理面向连接的网络服务时,例如 TCP ,交换数据之前必须在请求的进程套接字和提供服务的进程套接字之间建立连接。TCP 客户端可以调用函数connect 来建立与 TCP 服务器端的一个连接。该函数的描述如下:

#include <sys/socket.h>  
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

函数功能:创建套接字描述符;

返回值:若成功则返回套接字非负描述符,若出错返回-1;

说明:

sockfd是系统调用的套接字描述符,即由socket函数返回的套接字描述符;

servaddr是目的套接字的地址,该套接字地址结构必须包含目的IP地址和目的端口号,即想与之通信的服务器地址;

addrlen是目的套接字地址的大小;

TCP 客户端在调用函数 connect 前不必非得调用 bind 函数,因为内核会确定源 IP 地址,并选择一个临时端口作为源端口号。若 TCP 套接字调用connect 函数将建立 TCP 连接(执行三次握手),而且仅在连接建立成功或出错时才返回

3、bind 函数

调用函数 socket 创建套接字描述符时,该套接字描述符是存储在它的协议族空间中,没有具体的地址,要使它与一个地址相关联,可以调用函数bind 使其与地址绑定。客户端的套接字关联的地址一般可由系统默认分配,因此不需要指定具体的地址。若要为服务器端套接字绑定地址,可以通过调用函数 bind 将套接字绑定到一个地址。下面是该函数的描述:

#include <sys/socket.h>   
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数功能:将协议地址绑定到一个套接字;其中协议地址包含IP地址和端口号;

返回值:若成功则返回0,若出错则返回-1;

说明:

sockfd 为套接字描述符;

addr是一个指向特定协议地址结构的指针;

addrlen是地址结构的长度;

对于 TCP 协议,调用 bind 函数可以指定一个端口号,或指定一个 IP 地址,也可以两者都指定,还可以都不指定。若 TCP 客户端或服务器端不调用bind 函数绑定一个端口号,当调用connect 或 listen 函数时,内核会为相应的套接字选择一个临时端口号。一般 TCP 客户端使用内核为其选择一个临时的端口号,而服务器端通过调用bind 函数将端口号与相应的套接字绑定。

进程可以把一个特定的 IP 地址捆绑到它的套接字上,但是这个 IP 地址必须属于其所在主机的网络接口之一。对于 TCP 客户端,这就为在套接字上发送的 IP 数据报指派了源 IP 地址。对于 TCP 服务器端,这就限定该套接字只接收那些目的地为这个 IP 地址的客户端连接。TCP 客户端一般不把 IP 地址捆绑到它的套接字上。当连接套接字时,内核将根据所用外出网络接口来选择源 IP 地址,而所用外出接口则取决于到达服务器端所需的路径。若 TCP 服务器端没有把 IP 地址捆绑到它的套接字上,内核就把客户端发送的 SYN 的目的 IP 地址作为服务器端的源 IP 地址。

4、listen 函数

在编写服务器程序时需要使用监听函数 listen 。服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。listen 函数描述如下:

#include <sys/socket.h>   
int listen(int sockfd, int backlog);//若成功则返回0,若出错则返回-1;  

说明:

sockfd是套接字描述符;

backlog是该进程所要入队请求的最大请求数量;

listen 函数仅由 TCP 服务器调用,它有以下两种作用:

当 socket 函数创建一个套接字时,若它被假设为一个主动套接字,即它是一个将调用connect 发起连接的客户端套接字。listen 函数把一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求;

listen 函数的第二个参数规定内核应该为相应套接字排队的最大连接个数;

5、accept 函数

accept 函数由 TCP 服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。

该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示 TCP 三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。该函数描述如下:

#include <sys/socket.h>   
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

说明:

参数 cliaddr 和 addrlen 用来返回已连接的对端(客户端)的协议地址;

该函数返回套接字描述符,该描述符连接到调用connect函数的客户端;

这个新的套接字描述符和原始的套接字描述符sockfd具有相同的套接字类型和地址族,而传给accept函数的套接字描述符sockfd没有关联到这个链接,而是继续保持可用状态并接受其他连接请求;

若不关心客户端协议地址,可将cliaddr和addrlen参数设置为NULL

这个函数的第三个参数是值-结果参数,在这篇文章中讲到了值-结果参数类型,其实就是为了利用函数参数传递更多的信息

下面利用值-结果参数来写一个程序,显示客户的IP地址和端口号:

#include    "unp.h"
#include    <time.h>

int
main(int argc, char **argv)
{
    int                    listenfd, connfd;
    socklen_t            len;
    struct sockaddr_in    servaddr, cliaddr;
    char                buff[MAXLINE];
    time_t                ticks;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

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

    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ);

    for ( ; ; ) {
        len = sizeof(cliaddr);
        connfd = Accept(listenfd, (SA *) &cliaddr, &len);
        printf("connection from %s, port %d\n",
               Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
               ntohs(cliaddr.sin_port));

        ticks = time(NULL);
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
        Write(connfd, buff, strlen(buff));

        Close(connfd);
    }
}

客户端运行:

wu@ubuntu:~/opt/unpv13e/intro$ ./daytimetcpcli 127.0.0.1
Tue Oct 25 03:54:14 2016
wu@ubuntu:~/opt/unpv13e/intro$ ./daytimetcpcli 192.168.111.142
Tue Oct 25 03:54:29 2016

服务器端输出:

wu@ubuntu:~/opt/unpv13e/intro$ sudo ./daytimetcpsrv1
connection from 127.0.0.1, port 35361
connection from 192.168.111.142, port 53020

6、fork 和 exec 函数

fork函数调用一次若成功则返回两个值:

在调用进程(即父进程)中,返回新创建进程(即子进程)的进程ID;在子进程返回值是0;因此,可以根据返回值判断进程是子进程还是父进程;

#include <unistd.h>   
pid_t fork(void);

exec 序列函数

函数功能:把当前进程替换为一个新的进程,新进程与原进程ID相同;

返回值:若出错则返回-1,若成功则不返回;

函数原型:

#include <unistd.h>   
int execl(const char *pathname, const char *arg, ...);   
int execv(const char *pathnam, char *const argv[]);   
int execle(const char *pathname, const char *arg, ... , char *const envp[]);   
int execve(const char *pathnam, char *const argv[], char *const envp[]);   
int execlp(const char *filename, const char *arg, ...);   
int execvp(const char *filename, char *const argv[]);

6 个函数的区别如下:

(1)待执行的程序文件是 文件名 还是由 路径名 指定;

(2)新程序的参数是 一一列出 还是由一个 指针数组 来引用;

(3)把调用进程的环境传递给新程序 还是 给新程序指定新的环境;

 

如果文章对您有帮助,欢迎点击下方按钮打赏作者

Comments

No comments yet.
To verify that you are human, please fill in "七"(required)