Categories: Python

Network Programming in Python: Sockets, Protocols, and Communication

In today’s interconnected digital landscape, network programming has become a cornerstone for developing responsive, efficient, and scalable applications. Understanding how to leverage the capabilities of Python for network programming is essential for any developer aiming to build robust systems. This article delves into the core concepts of **network programming in Python**, covering sockets, protocols, and communication techniques. Whether you’re a seasoned programmer or a beginner eager to master Python’s network capabilities, this comprehensive guide will provide valuable insights and practical examples to enhance your coding prowess.

Introduction to Network Programming in Python

Network programming in Python revolves around enabling communication between different devices over a network. It leverages various libraries and frameworks to send and receive data, fulfill tasks distributed across multiple machines, and create network services and applications. A fundamental building block of network programming in Python is the socket, which provides an interface to interact with network layers within the operating system.

In essence, a socket is an endpoint for sending or receiving data across a computer network. When dealing with network programming in Python, two primary modules are most often used: socket and selectors. The socket module implements the Berkeley sockets API, facilitating the creation of both client and server applications. The selectors module, on the other hand, provides high-level I/O multiplexing for monitoring multiple socket connections.

import socket

# Example: Creating a simple TCP/IP client
def simple_client():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect(('localhost', 8080))
        s.sendall(b'Hello, server!')
        data = s.recv(1024)
    print('Received', repr(data))

The above code demonstrates how to establish a basic client connection to a server running on localhost on port 8080. The socket is configured for TCP/IP communication using socket.SOCK_STREAM. Upon connecting, the client sends a message and awaits a response.

For a deeper understanding, the socket module’s documentation is an excellent resource: Python socket documentation.

Python network protocols play a crucial part in network programming — they define rules and conventions for communication between network devices. Commonly used protocols like TCP (Transmission Control Protocol) and UDP (User Datagram Protocol) are directly supported in Python. Each of these protocols serves different purposes: TCP is connection-oriented and ensures reliable data transfer, while UDP is connectionless and favors speed over reliability.

For instance, a simple UDP client can be written as follows:

import socket

# Example: Creating a simple UDP client
def simple_udp_client():
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
        s.sendto(b'Hello, server!', ('localhost', 8080))
        data, server = s.recvfrom(1024)
    print('Received from server:', repr(data))

Python’s standard library caters to a variety of network programming tasks, but several third-party libraries and frameworks offer extended capabilities. Twisted, asyncio, and SocketServer are notable mentions. Twisted is an event-driven networking engine that facilitates building scalable network applications, asyncio supports writing concurrent code using the async/await syntax, and SocketServer provides basic classes for implementing network servers in a straightforward way.

Given the prevalence of networking tasks in modern software development, having a solid grasp of these tools and techniques is vital. Documentation and tutorials, such as the comprehensive Python Networking Programming Guide available on RealPython, can provide a roadmap for mastering the intricacies involved.

To get started with network programming in Python, ensure that you have a solid foundation in Python basics and a willingness to delve into the world of network protocols, communication methods, and socket-based programming.

Understanding Python Sockets: Basics and Examples

Sockets are a fundamental aspect of network programming in Python, providing a way for programs to communicate over a network. At a high level, a socket is an endpoint for sending or receiving data across a computer network. Understanding how sockets work in Python is essential for developing robust network applications. This section delves into the basics of Python sockets and presents concrete examples to illustrate their usage.

Python Sockets: Essentials

In Python, the socket module provides a low-level interface for network programming. The key functions involved in socket operations include:

  • socket.socket(family, type, proto=0): Creates a new socket using the specified address family, socket type, and protocol number. Common address families include AF_INET for IPv4 and AF_INET6 for IPv6. The types can be SOCK_STREAM for TCP and SOCK_DGRAM for UDP.
  • socket.bind(address): Binds the socket to an address (IP address and port number).
  • socket.listen(backlog): Enables a server to accept connections. The backlog parameter specifies the number of unaccepted connections that the system will allow before refusing new connections.
  • socket.accept(): Accepts a connection. It returns a new socket object and the address of the client.
  • socket.connect(address): Initiates a connection to a remote socket at the given address.
  • socket.send(bytes): Sends data to the connected socket.
  • socket.recv(bufsize): Receives data from the socket.
  • socket.close(): Closes the socket.

Python Network Example: TCP Echo Server and Client

Let’s explore a basic example showcasing a TCP echo server and client. The server listens for incoming connections and echoes any received data back to the client. The client connects to the server, sends a message, and then prints the server’s response.

TCP Echo Server

import socket

def start_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('localhost', 12345))
    server_socket.listen(5)
    print("Server is listening on port 12345...")
    
    while True:
        client_socket, client_address = server_socket.accept()
        print(f"Connection from {client_address} has been established.")
        
        while True:
            data = client_socket.recv(1024)
            if not data:
                break
            print(f"Received: {data.decode('utf-8')}")
            client_socket.sendall(data)  # Echo the received data
        
        client_socket.close()

if __name__ == "__main__":
    start_server()

TCP Echo Client

import socket

def start_client():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(('localhost', 12345))
    
    message = "Hello, Server!"
    client_socket.sendall(message.encode('utf-8'))
    
    data = client_socket.recv(1024)
    print(f"Received from server: {data.decode('utf-8')}")
    
    client_socket.close()

if __name__ == "__main__":
    start_client()

In this example, the server binds to localhost on port 12345 and listens for incoming connections. When a client connects, it waits to receive data and then echoes it back to the client. The client connects to the server on the same address and port, sends a message, and prints the echoed response.

Error Handling and Best Practices

Error Handling

Proper error handling is critical in network programming to ensure reliability. Use try-except blocks to catch and handle exceptions. For example:

try:
    server_socket.bind(('localhost', 12345))
except socket.error as e:
    print(f"Socket error: {e}")
    server_socket.close()
    exit(1)

Best Practices

  • Resource Management: Ensure sockets are closed properly using the with statement or finally block.
  • Concurrency: For handling multiple clients, consider using threading or asynchronous frameworks like asyncio.
  • Security: Validate data and sanitize inputs to prevent injection attacks. Use secure protocols like TLS/SSL for sensitive data transmission.

For more details on Python socket programming, refer to the official documentation.

This concise overview and example should help you grasp the essentials of Python socket programming, setting the foundation for more advanced topics in network programming.

Overview of Python Network Protocols

Python provides robust support for network programming, and one of its core strengths lies in the variety of network protocols it supports. Understanding these protocols is crucial for developing efficient and effective network applications. Here’s an in-depth look at some of the main Python network protocols and how to work with them.

HTTP and HTTPS Protocols

HTTP (Hypertext Transfer Protocol) and its secure variant HTTPS are the backbone of the web. Python’s requests library simplifies interactions with these protocols. Here’s a basic example of making a GET request:

import requests

response = requests.get('https://api.github.com')
print(response.status_code)
print(response.json())

For more advanced uses such as handling cookies, sessions, and authentication methods, refer to the requests documentation.

FTP Protocol

FTP (File Transfer Protocol) is used to transfer files between systems over the network. Python’s built-in ftplib module provides a simple way to handle FTP operations. Below is an example of how to download a file from an FTP server:

from ftplib import FTP

ftp = FTP('ftp.dlptest.com')
ftp.login()  # Public demo FTP server doesn't need a username and password
ftp.retrbinary('RETR somefile.txt', open('somefile.txt', 'wb').write)
ftp.quit()

Refer to the official ftplib documentation for more complex operations such as uploading files, directory listing, and error handling.

SMTP Protocol

SMTP (Simple Mail Transfer Protocol) is used for sending emails. Python’s smtplib module allows you to send emails using SMTP. Here is an example of sending an email:

import smtplib
from email.mime.text import MIMEText

msg = MIMEText('This is a test email.')
msg['Subject'] = 'Test Email'
msg['From'] = 'your_email@example.com'
msg['To'] = 'recipient@example.com'

server = smtplib.SMTP('smtp.example.com', 587)
server.starttls()
server.login('your_email@example.com', 'your_password')
server.sendmail('your_email@example.com', ['recipient@example.com'], msg.as_string())
server.quit()

For more advanced emailing capabilities, such as handling attachments and HTML content, refer to the smtplib documentation.

DNS Protocol

DNS (Domain Name System) is used for translating domain names to IP addresses and vice versa. Python’s dnspython library allows you to perform DNS queries and lookups efficiently. Here’s an example of performing a basic DNS lookup:

import dns.resolver

result = dns.resolver.resolve('example.com', 'A')
for ipval in result:
    print('IP', ipval.to_text())

For more advanced querying options, including MX, TXT, and CNAME record lookups, review the dnspython documentation.

MQTT Protocol

MQTT (Message Queuing Telemetry Transport) is designed for lightweight, low-bandwidth publish/subscribe messaging. Python’s paho-mqtt library can help you establish MQTT communications. Here’s an example:

import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, rc):
    print("Connected with result code " + str(rc))
    client.subscribe("topic/test")

def on_message(client, userdata, msg):
    print(msg.topic + " " + str(msg.payload))

client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message

client.connect("mqtt.eclipse.org", 1883, 60)
client.loop_forever()

The paho-mqtt documentation provides extensive details on authentication, QoS levels, and handling drops in connection.

Conclusion

Familiarity with these core Python network protocols not only helps develop a wide array of network applications but also leverages Python’s utilities to handle specific protocol-related tasks efficiently. These examples provide a foundation, but diving into the documentation and experimenting hands-on will cement your understanding and proficiency.

I have referenced the specific documentation for deeper dives into each protocol and have avoided discussing the basics of sockets or TCP/UDP, as these are covered in your specified sections. This ensures the content remains unique and focused on network protocols only.

TCP/IP Programming in Python: A Step-by-Step Guide

TCP/IP is the cornerstone of most modern network communications, and understanding how to implement this protocol in Python can greatly enhance your capabilities as a network programmer. Here we’ll guide you through creating a simple yet robust TCP server and client using Python’s socket library.

Setting Up the Environment

Before diving into TCP/IP programming, ensure you have Python installed on your machine. You can verify the installation by running python --version in your terminal. Install the socket library if it’s not already included in your Python distribution.

pip install socket

Creating a TCP Server

Let’s start with setting up a TCP server. The server will listen for incoming connections from clients, accept messages, and send responses back. Here is a basic example:

import socket

def start_server(host='127.0.0.1', port=65432):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind((host, port))
        s.listen()

        print(f"Server started at {host}:{port}")
        
        conn, addr = s.accept()
        with conn:
            print('Connected by', addr)
            while True:
                data = conn.recv(1024)
                if not data:
                    break
                print(f"Received: {data.decode()}")
                conn.sendall(data)  # Echoing back the received data

if __name__ == "__main__":
    start_server()

Detailed Steps

  1. Socket Creation: socket.socket(socket.AF_INET, socket.SOCK_STREAM) creates a socket object for TCP communication.
  2. Binding: s.bind((host, port)) binds the server to the chosen host and port.
  3. Listening: s.listen() enables the server to accept connections.
  4. Accepting Connections: conn, addr = s.accept() waits for an incoming connection and returns a new socket object representing the connection and the address of the client.
  5. Communication Loop: The loop while True keeps the server running. It uses conn.recv(1024) to read data from the client, and conn.sendall(data) to send the data back.

Creating a TCP Client

On the client-side, you need to create a connection to the server and communicate with it. The following code demonstrates a simple client:

import socket

def start_client(server_host='127.0.0.1', server_port=65432):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((server_host, server_port))
        print(f"Connected to {server_host}:{server_port}")
        
        while True:
            message = input("Enter message to send (type 'exit' to close): ")
            if message.lower() == 'exit':
                break
            s.sendall(message.encode())
            data = s.recv(1024)
            print(f"Received response: {data.decode()}")

if __name__ == "__main__":
    start_client()

Detailed Steps

  1. Socket Creation: Similar to the server, use socket.socket(socket.AF_INET, socket.SOCK_STREAM).
  2. Connecting: The s.connect((server_host, server_port)) method makes a connection to the server specified by server_host and server_port.
  3. Sending and Receiving Data: Data is sent and received using s.sendall(message.encode()) and s.recv(1024) respectively.

Error Handling and Robustness

In production code, you’ll want comprehensive error handling around network operations. Use try-except blocks to catch exceptions and ensure sockets are properly closed.

import socket

def start_client(server_host='127.0.0.1', server_port=65432):
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect((server_host, server_port))
            print(f"Connected to {server_host}:{server_port}")
            
            while True:
                message = input("Enter message to send (type 'exit' to close): ")
                if message.lower() == 'exit':
                    break
                s.sendall(message.encode())
                data = s.recv(1024)
                print(f"Received response: {data.decode()}")
    except ConnectionError as e:
        print(f"Connection error: {e}")
    except Exception as e:
        print(f"An error occurred: {e}")
    finally:
        s.close()

if __name__ == "__main__":
    start_client()

Resources for Further Reading

By following these steps, you’ll be well on your way to mastering TCP/IP programming in Python, enabling you to develop sophisticated network-based applications.

UDP Programming with Python: Practical Use Cases

User Datagram Protocol (UDP) is a vital protocol in network programming, particularly when low-latency communication is required and the occasional loss of data can be tolerated. Unlike TCP, UDP is connectionless and does not guarantee delivery, making it suitable for applications like live video streaming, voice over IP (VoIP), and gaming. Let’s explore some practical use cases of UDP programming with Python.

Streaming Data

In streaming applications, low latency is crucial, and UDP is often the protocol of choice for this purpose. Below is an example of a simple UDP server that streams messages:

import socket

# Set up a UDP server
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('localhost', 6789)
server_socket.bind(server_address)

print("UDP server up and listening on port 6789")

while True:
    message, client_address = server_socket.recvfrom(2048)
    print(f"Received message: {message} from {client_address}")
    # Optionally send a response to the client
    server_socket.sendto(message, client_address)

And the corresponding client:

import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('localhost', 6789)

message = b'Hello, UDP server!'
client_socket.sendto(message, server_address)

response, server = client_socket.recvfrom(2048)
print(f"Received response from server: {response}")

client_socket.close()

Real-time Applications

Real-time applications such as online gaming or real-time analytics rely on UDP for its speed and efficiency. Here is an example of a simple ping-pong style game communication using UDP:

# game_server.py
import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind(('localhost', 9000))

print("Game server started on UDP port 9000")

while True:
    data, addr = server_socket.recvfrom(1024)
    print(f"Player {addr} sent: {data.decode()}")

    if data:
        server_socket.sendto(b'PONG', addr)
# game_client.py
import socket
import time

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_addr = ('localhost', 9000)
msg = b'PING'

while True:
    client_socket.sendto(msg, server_addr)
    print(f"Sent: {msg.decode()}")
    
    data, server = client_socket.recvfrom(1024)
    print(f"Received from server: {data.decode()}")
    
    time.sleep(1)

Service Discovery

UDP broadcasts are commonly used for service discovery in local networks. Devices can quickly announce their presence and available services without establishing a connection. In Python, this can be achieved using broadcast addresses. Here’s a short example:

# discovery_server.py
import socket

broadcast_ip = "255.255.255.255"
broadcast_port = 37020

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
server_socket.bind(("", broadcast_port))

print("Service discovery responder running...")

while True:
    message, addr = server_socket.recvfrom(1024)
    print(f"Discovery message from {addr}: {message.decode()}")
    server_socket.sendto(b"Service available", addr)
# discovery_client.py
import socket

broadcast_ip = "255.255.255.255"
broadcast_port = 37020
message = b"Discover services"

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

client_socket.sendto(message, (broadcast_ip, broadcast_port))
print("Broadcast message sent")

response, server = client_socket.recvfrom(1024)
print(f"Received response: {response} from {server}")

These examples showcase practical use cases where UDP can be effectively used for streaming, real-time applications, and service discovery, underlining the unique strengths of UDP in Python network programming. For further reading, refer to the official Python socket documentation.

Advanced Network Communication Techniques in Python

Advanced network communication techniques in Python can greatly enhance your applications’ efficiency, reliability, and performance. These techniques move beyond the basics of socket creation and handling with more sophisticated strategies such as non-blocking sockets, multiplexing, and secure socket layer (SSL) encryption. Here, we’ll delve into some of these advanced methods.

Non-Blocking Sockets

Non-blocking sockets allow a socket to perform other operations while it’s waiting for network events. This is particularly useful in scenarios where latency can impact performance, and you don’t want to halt the execution of your program waiting for I/O operations to complete.

import socket

# Create a non-blocking socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setblocking(False)

try:
    s.connect(('example.com', 80))
except BlockingIOError:
    pass

# Perform other tasks while the socket is connecting...

Multiplexing with select

Python’s select module allows you to monitor multiple sockets and handle I/O operations using a single thread. This is exceedingly helpful in server applications that must handle many clients simultaneously.

import socket
import select

s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s1.bind(('localhost', 8000))
s2.bind(('localhost', 8001))

s1.listen()
s2.listen()

sockets_list = [s1, s2]

while True:
    read_sockets, _, _ = select.select(sockets_list, [], [])
    for sock in read_sockets:
        client_socket, client_address = sock.accept()
        print(f"Connection from {client_address}")
        client_socket.send(b"Hello from server")
        client_socket.close()

SSL Encryption

Secure Sockets Layer (SSL) is crucial for protecting data transferred between networked machines. Python’s ssl module provides a robust way to wrap socket objects for secure communication.

import socket
import ssl

# Wrap a socket using SSL
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
wrapped_socket = context.wrap_socket(sock, server_hostname='example.com')

wrapped_socket.connect(('example.com', 443))
wrapped_socket.sendall(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
response = wrapped_socket.recv(4096)
print(response.decode("utf-8"))
wrapped_socket.close()

Using Asynchronous I/O with asyncio

The asyncio module in Python offers a convenient way to handle asynchronous network communication. This is particularly useful when building applications that need to scale.

import asyncio

async def handle_client(reader, writer):
    data = await reader.read(100)
    message = data.decode("utf-8")
    writer.write(data)
    await writer.drain()
    writer.close()

async def main():
    server = await asyncio.start_server(handle_client, '127.0.0.1', 8888)
    async with server:
        await server.serve_forever()

asyncio.run(main())

HTTP with http.client

For applications that need to communicate over HTTP, Python offers the http.client module for making HTTP requests programmatically.

import http.client

conn = http.client.HTTPSConnection("www.example.com")

conn.request("GET", "/")
response = conn.getresponse()
print(response.status, response.reason)
data = response.read()
print(data.decode("utf-8"))
conn.close()

These advanced techniques can significantly optimize your network programming tasks in Python. They offer robust solutions for handling large-scale network operations, ensuring secure and efficient communication. For more details, refer to the Python official documentation for socket programming and SSL.

Troubleshooting and Debugging Python Network Development

Effective Troubleshooting and Debugging in Python Network Development is crucial for identifying and resolving issues that arise during the development of network applications. Here are some common techniques and tools for troubleshooting and debugging:

Tool Utilization for Debugging

Wireshark:
Wireshark is an essential tool for network troubleshooting that enables you to capture and analyze network packets.

To get started with Wireshark:

  1. Download and install Wireshark from the official website.
  2. Launch Wireshark and select the network interface you wish to monitor.
  3. Apply filters to streamline data, such as filtering out specific ports or IP addresses relevant to your Python application.
    ip.addr == 192.168.1.1 && tcp.port == 8000
    

tcpdump:
Tcpdump is another powerful command-line packet analyzer. It can be used to capture packets for offline analysis or directly view packet details.

sudo tcpdump -i eth0 host 192.168.1.1 and port 8000 -w output.pcap

You can then open the output.pcap file in Wireshark for detailed analysis.

Logging and Verbose Output

Leveraging Python’s logging module to capture detailed logs of network activity can be incredibly valuable:

import logging

logging.basicConfig(level=logging.DEBUG, filename='network_debug.log', 
                    format='%(asctime)s %(levelname)s:%(message)s')

def send_data(socket, data):
    try:
        socket.sendall(data)
        logging.info(f"Sent data: {data}")
    except Exception as e:
        logging.error(f"Error sending data: {e}")

def receive_data(socket):
    try:
        data = socket.recv(1024)
        logging.info(f"Received data: {data}")
    except Exception as e:
        logging.error(f"Error receiving data: {e}")

This example captures and logs the sending and receiving data events, assisting in post-incident analysis.

Handling Network Errors

Properly handling exceptions and network errors can prevent your application from crashing and can provide context on where exactly the problem occurred:

import socket

try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('example.com', 80))
except socket.error as e:
    logging.error(f"Socket error: {e}")
except Exception as e:
    logging.error(f"General error: {e}")

By catching specific exceptions such as socket.error, you can provide more insight into the nature of the error rather than catching a generic exception.

Network Emulators and Simulators

Network Simulation Tools: Tools like Mininet can be used to create a virtual network that allows you to test the behavior of your Python network application in various conditions.

sudo mn

With Mininet, you can simulate network behavior such as link failures, varying latencies, and bandwidth limitations that your Python application will face in real networks.

Using Python Network Debugging Libraries

Python offers libraries that can assist in network debugging:

  • Scapy: A powerful Python library used for network packet generation, manipulation, and decoding.
    from scapy.all import *
    
    def debug_packet(packet):
        if packet.haslayer(IP):
            ip_layer = packet.getlayer(IP)
            logging.info(f"Source IP: {ip_layer.src}, Destination IP: {ip_layer.dst}")
    
    sniff(prn=debug_packet, filter="tcp", store=0)
    

    This code captures and logs TCP packets, including their source and destination IP addresses.

Debugging with IDEs

IDEs like PyCharm and VSCode offer integrated debugging tools. You can set breakpoints, inspect variables, and step through your code to identify where issues exist.

  • PyCharm:
    1. Set breakpoints in your network-related code sections.
    2. Run your script in debug mode.
    3. Use the built-in tools to inspect socket states and variable values.

By incorporating the above techniques and tools, you can systematically identify, diagnose, and resolve issues in your Python network development projects, ensuring robust and reliable network applications.

Vitalija Pranciškus

Recent Posts

Navigating the Top IT Careers: A Guide to Excelling as a Software Engineer in 2024

Discover essential insights for aspiring software engineers in 2023. This guide covers career paths, skills,…

1 month ago

Navigating the Future of Programming: Insights into Software Engineering Trends

Explore the latest trends in software engineering and discover how to navigate the future of…

1 month ago

“Mastering the Art of Software Engineering: An In-Depth Exploration of Programming Languages and Practices”

Discover the essentials of software engineering in this comprehensive guide. Explore key programming languages, best…

1 month ago

The difference between URI, URL and URN

Explore the distinctions between URI, URL, and URN in this insightful article. Understand their unique…

1 month ago

Social networks steal our data and use unethical solutions

Discover how social networks compromise privacy by harvesting personal data and employing unethical practices. Uncover…

1 month ago

Checking if a checkbox is checked in jQuery

Learn how to determine if a checkbox is checked using jQuery with simple code examples…

1 month ago