在通信过程中,套接字一定是成对出现的即服务器一个,客户端一个。Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。
虽然仅有一个套接字,但这个套接字有两个缓冲区,一端的发送缓冲区对应对端的接收缓冲区,那么不看细节,那就是管道从一端流到另一端。
一个服务端和一个客户端连接有三个socket,两个在服务端,一个在客户端,服务端使用socket函数创建完socket并bind地址结构在connect设置监听上限 然后交给accept,在有连接建立后,accept将新建一个socket,旧socket将继续去监听。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
如果本地存储使用小端字节序,需要进行网络字节序和主机字节序的转换。
#include
uint32_t htonl(uint32_t hostlong); //不经常使用,使用inet_pton
uint16_t htons(uint16_t hostshort);//客户端和服务端指定端口时使用
uint32_t ntohl(uint32_t netlong); //不经常使用,使用inet_ntop
uint16_t ntohs(uint16_t netshort);//服务端获取客户端端口时使用
htonl 本地ip转网络ip
htons 本地端口转网络端口
ntohl 网络ip转本地ip
ntohs 网络端口转本地端口
n代表网络,h代表本地,l代表ip,s代表端口,ip不是点分十进制
上述在转换ip时还需要将点分十进制string类型转换成int类型,比较麻烦
并且只能处理ipv4的地址,现在在转换ip时使用下面的函数
#include
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
inet_pton,这里的p表示点分十进制的ipv4或者ipv6,n表示网络字节序,因此这个命令表示将点分十进制的ip转换成网络字节序。
inet_ntop 顾名思义将网络字节序转换成点分十进制的形式。
具体看下面的ip地址转换函数。
上述转换函数在sockaddr_in中使用
在linxu使用man inet_pton查看函数原型
//本地字节序(string)转换成网络字节序
int inet_pton(int af, const char *src, void *dst);
af表示使用的ip类型,有 AF_INET和 AF_INET6
src:(传入)表示ip地址(点分十进制)
dst:(传出)转换后的网络字节序的ip地址
返回值:1:成功 ; 0:表示src不是一个有效的ip地址;-1:失败
在linux使用man inet_ntop查看函数原型
//网络字节序(二进制)转换成本地字节序(string)
#include
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
af表示使用的ip类型,有 AF_INET和 AF_INET6
src:(传入)网络字节序ip地址
dst:(传出)本地字节序(string)
size: dst的大小
返回值:成功返回一个dst,失败返回空
//一般代码
struct sockaddr_in addr;
bind(fd,(struct sockaddr*)&addr,);//第三个参数是size,这里先不说
//强制将sockaddr_in转换成sockaddr类型
man有9卷,查看ip看第7卷,即man 7 ip
定义的时候,由于addr.sin_addr是一个结构体,应该给结构体里的数据赋值而不是给结构体,这里注意。
struct sockaddr_in addr;
addr.sin_family= AF_INET;//或者是 AF_INET6
addr.sin_port=htons(9527);//这里随便指定了一个端口 本地端口转网络端口h本地 n网络 s端口
addr.sin_addr.s_addr=inet_pton(AF_INET,"192.168.1.101",dst);
上述的addr.sin_addr.s_addr可以那样写,也可以
int dst;
inet_pton(AF_INET,"192.168.1.101",(void *)&dst);
addr.sin_addr.s_addr=dst;
INADDR_ANY;//取出系统中有效的任意ip地址,取出的是二进制类型
//因此还要转换成网络字节序
addr.sin_addr.s_addr=(htonl)INADDR_ANY;
服务端
socket()产生一个套接字,由文件描述符关联
bind()将套接字和ip、端口绑定
listen()设置同时监听上限(说白了就是参数设置,并不是监听)
accept()阻塞监听客户端连接
套接字给了accept作为参数,当与客户端建立连接后会返回一个新的socket,那个旧的socket会被解放出来继续监听,这个旧socket就像酒店迎宾小姐,引着人进入酒店后会重新去监听。
read()读数据
write()回写数据
close()关闭连接
客户端
socket()创建socket
connect()socket要连接的ip和端口
write()
read()
close()
man socket
#include /* See NOTES */
#include
int socket(int domain, int type, int protocol);
功能:socket 创建一个套接字
domain:常见下面的三种
type:指定传输的协议 SOCK_STREAM / SOCK_DGRAM
protocol: 选用的协议中代表协议 默认传0,传0便是根据type指定的选协议
SOCK_STREAM 便是TCP,SOCK_DGRAM便是UDP
返回值:成功返回一个新套接字对应的文件描述符 ,失败是返回-1
fd= socket(AF_INET, SOCK_STREAM , 0);
man 2 bind
#include /* See NOTES */
#include
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:给socket绑定一个ip+端口(地址结构)
sockfd:socket()的返回值
addr:先初始化sockaddr_in,再强转成sockaddr ,下面是个例子
下面初始化sockaddr_in
struct sockaddr_in addr;
addr.sin_family= AF_INET;//或者是 AF_INET6
addr.sin_port=htons(9527);//这里随便指定了一个端口 本地端口转网络端口h本地 n网络 s端口
addr.sin_addr.s_addr=(htonl)INADDR_ANY;
强转时
(struct sockaddr *)addr
addrlen:地址结构的大小
返回值:成功返回0 失败返回-1 error
man listen
#include /* See NOTES */
#include
int listen(int sockfd, int backlog);
功能:listen设置同时与服务器建立连接的上限数(同时进行三次握手的客户端数量)
sockfd:socket
backlog :上限值,上限值不能超过128
返回值:成功返回0 失败返回-1 error
man 2 accept
#include /* See NOTES */
#include
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:阻塞等待客户端建立连接,成功返回一个与客户端成功连接的新socket文件描述符
sockfd:给个socket,这里注意,新产生的socket要依托于旧socket,主要是用旧socket的ip和端口
addr:传出参数,传出成功与服务器建立连接的那个客户端的地址结构(ip+端口号)
addrlen:传入传出参数。进去时是addr的大小,出是客户端addr实际大小
socklen_t clit_addr_len =sizeof(addr);
//第三个参数如下面这样写
&clit_addr_len
返回值:成功时 返回能够与服务器进行数据通信的socket对应的文件描述符 失败:-1 error
man 2 connect
#include /* See NOTES */
#include
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
使用现有的socket与服务器建立连接
sockfd:socket
addr:传入参数,是服务器的地址结构(ip+端口)
addrlen:服务器地址结构的大小
客户端不需要绑定自己的ip和端口,虽然可以使用bind绑定
如果没有绑定,那么会采用“隐式绑定”(系统来干的)
注意上图强转时是传的地址,上图有个错误,在accept函数里面,第二个参数应该是用来保存客户端信息的。具体代码看后面。
编译
gcc tcpserver.c -o server -Wall -g
运行server,这里暂时使用脑残命令测试
可以看到运行成功
下面是代码,下面代码加了显示客户端ip地址和端口的功能
#include
#include
#include
#include
#include
#include
#include
#include
#include
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int lfd=0;
lfd=socket(AF_INET,SOCK_STREAM,0);
if(lfd==-1)
{
sys_err("socket error");
}
//bind函数绑定ip和端口
struct sockaddr_in server_addr;
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(22222);
server_addr.sin_addr.s_addr=(htonl)INADDR_ANY;
bind(lfd,(struct sockaddr *)&server_addr,sizeof(server_addr));
//设置同时监听个数
int lisRetrun=listen(lfd,128);
if(lisRetrun==-1)
{
sys_err("listen error");
}
//accept
struct sockaddr_in client_addr;
socklen_t cliAddrLen=sizeof(client_addr);
int cfd=accept(lfd,(struct sockaddr *)&client_addr,&cliAddrLen);
if(cfd==-1)
{
sys_err("accept error");
}
char client_ip[1024];
printf("\n client ip:%s port:%d\n",
inet_ntop(AF_INET,&client_addr.sin_addr,client_ip,sizeof(client_ip)),
ntohs(client_addr.sin_port)
);
char buf[BUFSIZ];
while(1)
{
int ret= read(cfd,buf,sizeof(buf));
int i;
for(i=0;i<ret;i++)
{
printf("%c",buf[i]);
buf[i]=toupper(buf[i]);
}
write(cfd,buf,ret);
}
close(lfd);
close(cfd);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int cfd=socket(AF_INET,SOCK_STREAM,0);
if(cfd==-1)
{
sys_err("create socket error");
}
struct sockaddr_in serv_addr;//服务器地址结构
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(22222);//设置端口
int dst;
inet_pton(AF_INET, "127.0.0.1", (void *)&dst);
serv_addr.sin_addr.s_addr=dst;
int conResult=connect(cfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
if(conResult)
{
sys_err("connect error");
}
int count=10;
char buf[1024];
while(--count)
{
write(cfd,"hello",5);
int ret= read(cfd,buf,sizeof(buf));
int j;
for(j=0;j<ret;j++)
{
printf("%c",buf[j]);
}
sleep(1);
printf("\n");
}
close(cfd);
return 0;
}