https://blog.youkuaiyun.com/hguisu/article/details/7445768/#
在文档的参考下加入了一些自己的理解。
一、TCP
1.TCP编程框架
服务器端先初始化socket,然后通过bind将socket返回的网络文件描述符和服务器的IP地址和端口号绑定起来,随后调用listen对socket返回的fd进行监听,调用accept阻塞,等待客户端接入,这是如果客户端也初始化了一个socket,然后通过connect连接服务器,此时客户端和服务器就连接成功了,然后就可以通过send/write、recv/read进行通信。
2.常用函数
2.1 socket()函数
int socket(int domain, int type, int protocol);
* domain用于设置网络通信的域,常用的协议簇有AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)
* type用来指定socket类型,常用的有SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)
* protocol用来指定协议,常用的协议有IPPROTO_TCP、IPPTOTO_UDP,分别对应TCP传输协议、UDP传输协议,通常传0选择type类型对应的协议
2.2.1 bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用来将socket返回的网络文件描述符和服务器的IP地址和端口号绑定起来
* sockfd为socket返回的fd
* addr为struct sockaddr类型的结构体指针,包含服务器的IP地址和端口号
* addrlen为第二个参数的长度
2.2.2 表示IP地址的相关数据结构
* 相关定义都定义在/usr/include/netinet/in.h中
* struct in_addr
{
in_addr_t s_addr;
};
* struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
}
* struct sockaddr 这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体,bind、connect等函数中都需要这个结构体,这个结构体是兼容IPV4和IPV6的。 在实际编程中这个结构体会被一个struct sockaddr_in或者一个struct sockaddr_in6所填充,即涉及到struct sockaddr的变量可以看实际使用 的是IPv4还是IPv6用sockaddr_in或者sockaddr_in6变量替代struct sockaddr_in表示使用的是IPv4,struct sockaddr_in6表示使用的是IPv6
* 在结构体中定义了端口号和IP地址,通常服务器在初始化时会通过bind给当前的fd绑定一个IP地址和端口号,由这两个元素可以识别一个特定主机中的某个应用程序/进程;而客户端通常不用指定端口号,由主机通过connect连接服务器时自动分配一个端口号和本机的IP地址组合。
* 关于网络字节序和主机字节序。即结构体中的IP地址和端口号(端口号为两字节数据时)均需要采用网络字节序。通常所说的主机字节序分为大端模式(高字节对应低地址)和小端模式(相反),网络字节序为在传输过程中低字节在前,高字节在后。实际编程中通常采用htons将端口号转换为网络字节序,通过inet_addr或者inet_pton将字符串形式的IP地址转换为32位网络字节序形式的二进制数
2.3 listen()函数
int listen(int sockfd, int backlog);
监听服务器socket返回的fd,第二个参数为相应的socket可以排队的最大连接数,socket函数创建的socket默认是主动类型的,listen将socket变为被动,等待客户端的连接请求
2.4.1 accept()函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
服务器阻塞等待客户端的接入,accept返回成功会返回一个连接套接字,可通过返回的连接套接字完成和客户端的通信
* sockfd为服务器socket返回的fd,属于监听套接字
* 第二个参数为输出参数,即结果参数,是一个结构体,用来输出客户端的IP地址和端口号,如果不想要可以传NULL
* 第三个参数也为输出型参数,用来指明第二个参数的大小,第二个参数为NULL时,它也传NULL
2.4.2 监听套接字和连接套接字
监听套接字:一个套接字会由主动连接的套接字经过listen后变为监听套接字,譬如accept的入参sockfd,是由服务器调用socket函数后经过listen形成的。
连接套接字:譬如accept返回的套接字,代表着网络中已经存在的点点连接,是服务器客户端相互通信的套接字。此外,连接套接字并没有占用新的端口和客户端通信,依然使用和监听套接字相同的端口号。
一个服务器通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户端连接创建了一个已连接socket描述字,当服务器完成了对某个客户端的服务,相应的已连接socket描述字就被关闭。
2.5 connect()函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数用来完成客户端和服务器的连接
* 第一个参数为客户端通过socket函数返回的文件描述符
* 第二个参数为一个结构体,其中包含了服务器的IP地址和端口号
* 第三个参数为第二个参数的长度
2.6 send()、recv()函数
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv函数为从sockfd中读数据到buf中,send函数为将buf中的数据发送给sockfd
2.7 close()函数
int close(int fd);
将fd描述符标记为-1,即关闭状态,该描述符不能再作为send或recv的第一个参数进行通信,注意close操作只是使相应socket描述符为-1,只有为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
2.8 socket编程实例
服务器:
/* File Name: server.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define DEFAULT_PORT 8000
#define MAXLINE 4096
int main(int argc, char** argv)
{
int socket_fd, connect_fd;
struct sockaddr_in servaddr;
char buff[4096];
int n;
//初始化Socket
if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
//初始化
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。
servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT
//将本地地址绑定到所创建的套接字上
if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
//开始监听是否有客户端连接
if( listen(socket_fd, 10) == -1){
printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
printf("======waiting for client's request======\n");
while(1){
//阻塞直到有客户端连接,不然多浪费CPU资源。
if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){
printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
continue;
}
//接受客户端传过来的数据
n = recv(connect_fd, buff, MAXLINE, 0);
//向客户端发送回应数据
if(!fork()){ /*紫禁城*/
if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1)
perror("send error");
close(connect_fd);
exit(0);
}
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
close(connect_fd);
}
close(socket_fd);
}
客户端:
/* File Name: client.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define MAXLINE 4096
int main(int argc, char** argv)
{
int sockfd, n,rec_len;
char recvline[4096], sendline[4096];
char buf[MAXLINE];
struct sockaddr_in servaddr;
if( argc != 2){
printf("usage: ./client <ipaddress>\n");
exit(0);
}
if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8000);
if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
printf("inet_pton error for %s\n",argv[1]);
exit(0);
}
if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
printf("send msg to server: \n");
fgets(sendline, 4096, stdin);
if( send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {
perror("recv error");
exit(1);
}
buf[rec_len] = '\0';
printf("Received : %s ",buf);
close(sockfd);
exit(0);
}
inet_pton为Linux中的IP地址转换函数,用来将点分十进制的IP地址转换为二进制,是inet_addr的扩展
int inet_pton(int af, const char *src, void *dst);//转换字符串到网络地址:
第一个参数为协议簇,函数将第二个参数传入的IP地址转换后存到dst中。
代码测试过程:
编译server.c gcc server.c -o server
启动进程 ./server
显示结果 ======waiting for client's request======
编译client.c gcc client.c -o client
启动进程 ./client 127.0.0.1
注意:
在ubuntu 编译源代码的时候,头文件types.h可能找不到。
使用dpkg -L libc6-dev | grep types.h 查看。
如果没有,可以使用
apt-get install libc6-dev安装。
如果有了,但不在/usr/include/sys/目录下,手动把这个文件添加到这个目录下就可以了。